-
Notifications
You must be signed in to change notification settings - Fork 204
[자동차 경주] 김지훈 미션 제출합니다. #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
eff3a21
94669f3
079c270
1cf4f1c
7910569
b8c8d11
812e193
d07a1c0
ec96c35
e41fa17
e77c073
eb04f07
4180e62
96fbbbc
10e48e4
c74388f
fc8936e
29ad891
0fd082f
f2ae844
d4ab24c
6c76a26
6e8eb9a
92fe804
a75190f
3519037
c1355ed
9af4068
a242ccf
0ccd23d
dd4e12a
d04b9c2
a93183e
9307360
4f58fa0
1ad10fd
ee782ff
efd9073
91fb602
9470f37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,78 @@ | ||
| # javascript-racingcar-precourse | ||
| # 🏎️ 자동차 경주 기능 명세 | ||
|
|
||
| ## 1. Domain | ||
|
|
||
| ### 1) `Car` | ||
|
|
||
| - 자동차의 이름과 이동 거리를 보유합니다. | ||
| - 전진 명령을 받아 이동 거리를 1 증가시킵니다. | ||
| - 자동차 이름과 현재 이동 거리를 반환합니다. | ||
|
|
||
| ## 2. Model | ||
|
|
||
| ### 1) `RaceSimulator` | ||
|
|
||
| - 자동차 이름과 시도 횟수를 받아 경주 전체를 관리합니다. | ||
| - `createCars` 함수를 사용해 자동차 목록을 생성합니다. | ||
| - 남은 시도 횟수를 추적하며, 각 턴마다 `runOneStep` 함수를 호출해 이동을 처리합니다. | ||
| - 각 단계의 자동차 위치 정보를 반환합니다. | ||
| - 최종 우승자를 계산해 반환합니다. | ||
|
|
||
| ### 2) `raceUtils` | ||
|
|
||
| - 경주 관련 로직을 유틸 함수 형식으로 제공합니다. | ||
|
|
||
| #### `createCars(carNames)` | ||
|
|
||
| - 쉼표로 구분된 자동차 이름 문자열을 입력받아, 각 이름의 공백을 제거합니다. | ||
| - 각 이름에 대해 `Car` 인스턴스를 생성해 배열로 반환합니다. | ||
|
|
||
| #### `runOneStep(cars)` | ||
|
|
||
| - 자동차 배열을 받아 한 번의 이동 시도를 처리합니다. | ||
| - 각 자동차마다 0~9 범위의 난수를 생성하고, | ||
| 난수가 4 이상일 경우 전진시킵니다. | ||
|
|
||
| ## 3. View | ||
|
|
||
| ### 1) `InputView` | ||
|
|
||
| - 사용자에게 입력 안내 메시지를 표시합니다. | ||
| - 자동차 이름들을 입력받습니다. | ||
| - 시도 횟수를 입력받습니다. | ||
|
|
||
| ### 2) `OutputView` | ||
|
|
||
| - 실행 결과 헤더를 출력합니다. | ||
| - 단계마다 자동차별 현재 위치를 출력합니다. | ||
| - 최종 우승자를 출력합니다. | ||
|
|
||
| ## 4. Controller | ||
|
|
||
| ### 1) `RaceController` | ||
|
|
||
| - `InputView`를 통해 자동차 이름과 시도 횟수를 입력받습니다. | ||
| - `RaceSimulator`를 생성하여 경주를 초기화합니다. | ||
| - `OutputView`로 결과 헤더를 출력합니다. | ||
| - 남은 시도가 있는 동안 반복하여 각 단계를 실행합니다. | ||
| - 각 단계의 결과를 `OutputView`로 출력합니다. | ||
| - 경주 종료 후 우승자를 `OutputView`로 출력합니다. | ||
|
|
||
| ## 5. 예외 처리 | ||
|
|
||
| ### 1) 자동차 이름 검증 | ||
|
|
||
| - 자동차 이름 목록이 쉼표로 구분된 올바른 형식이 아닌 경우 예외를 발생시킵니다. | ||
| - 자동차 이름의 길이를 검증합니다. | ||
| - 이름이 5자를 초과하는 경우 예외를 발생시킵니다. | ||
| - 이름이 비어있는 경우 예외를 발생시킵니다. | ||
| - 이름이 중복된 경우 예외를 발생시킵니다. | ||
|
|
||
| ### 2) 시도 횟수 검증 | ||
|
|
||
| - 시도 횟수가 숫자가 아닌 경우 예외를 발생시킵니다. | ||
| - 시도 횟수가 0 이하인 경우 예외를 발생시킵니다. | ||
|
|
||
| ### 3) 에러 메시지 | ||
|
|
||
| - 모든 예외는 `[ERROR]`로 시작하는 메시지를 제공합니다. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,10 @@ | ||
| import RaceController from './controllers/RaceController.js'; | ||
|
|
||
| class App { | ||
| async run() {} | ||
| async run() { | ||
| const controller = new RaceController(); | ||
| await controller.start(); | ||
| } | ||
| } | ||
|
Comment on lines
+4
to
8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. App에서 의도적으로 try...catch를 빼신 건지 궁금합니다! 물론 굳이 쓰지 않아도 throw error에서 에러를 던지며 프로그램이 종료되긴 하지만 그 종료된 이후의 흐름을 제어하기 위해선 error throw시 상위 코드에서 catch 문이 필수라고 생각해서요!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1주차 미션에서는 테스트를 통과하기 위해 컨트롤러에서 try-catch로 예외를 잡은 뒤, 다만 이 구조가 조금 어색하다고 느껴져서, 2주차 미션에서는 메시지 출력 후 예외를 다시 던지는 부분은 제거했습니다. 말씀해 주신 것처럼 별도의 에러 상황에 대한 예외 처리가 필요하지 않아서 상위 레벨에서 try-catch를 사용하지 않았습니다. |
||
|
|
||
| export default App; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 에러 메세지가 굉장히 이해하기 쉽게 선언 되어 있는게 인상 깊습니다. 따로 참고하고 계신 자료가 있으실까요? 아니면 고민해서 작성하시는 편이실까요? 저도 이렇게 네이밍 하고 싶어 여쭤봅니다...
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 따로 참고하고 있는 자료는 없고, 평소에 네이밍에 시간을 오래 쓰는 것 같아요 🥹 기능 구현이 끝난 후에 함수명이나 변수명을 다시 검토하는 것도 좋은 습관인 것 같아요~ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| const ERROR_PREFIX = '[ERROR]'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저도 다음부터는 prefix로 만들어서 사용해봐야겠어요! |
||
|
|
||
| const ERROR_MESSAGES = Object.freeze({ | ||
| NAME_TOO_LONG: `${ERROR_PREFIX} 자동차 이름은 5자 이하여야 합니다.`, | ||
| NAME_EMPTY: `${ERROR_PREFIX} 자동차 이름을 입력해야 합니다.`, | ||
| NAME_LIST_FORMAT_INVALID: `${ERROR_PREFIX} 자동차 이름은 쉼표로 구분된 올바른 형식으로 입력해야 합니다.`, | ||
| NAME_DUPLICATED: `${ERROR_PREFIX} 자동차 이름은 중복될 수 없습니다.`, | ||
| ATTEMPT_COUNT_EMPTY: `${ERROR_PREFIX} 시도 횟수를 입력해야 합니다.`, | ||
| ATTEMPT_COUNT_NON_NUMERIC: `${ERROR_PREFIX} 시도 횟수는 숫자만 입력해야 합니다.`, | ||
| ATTEMPT_COUNT_NOT_POSITIVE: `${ERROR_PREFIX} 시도 횟수는 0보다 커야 합니다.`, | ||
| ATTEMPT_COUNT_NOT_INTEGER: `${ERROR_PREFIX} 시도 횟수는 정수로 입력해야 합니다.`, | ||
| }); | ||
|
|
||
| export default ERROR_MESSAGES; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| const PROMPT_MESSAGES = Object.freeze({ | ||
| CAR_NAMES_INPUT: '경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)\n', | ||
| ATTEMPT_COUNT_INPUT: '시도할 횟수는 몇 회인가요?\n', | ||
| RESULT_HEADER: '\n실행 결과', | ||
| FINAL_WINNERS_PREFIX: '최종 우승자', | ||
| }); | ||
|
|
||
| export default PROMPT_MESSAGES; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| const RACE_CONFIG = Object.freeze({ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 하드 코딩 대신 상수로 잘 정의하시는 것 같아요! |
||
| MOVE_THRESHOLD: 4, | ||
| MIN_RANDOM_VALUE: 0, | ||
| MAX_RANDOM_VALUE: 9, | ||
| MAX_CAR_NAME_LENGTH: 5, | ||
| }); | ||
|
|
||
| export default RACE_CONFIG; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| const TEST_CONFIG = Object.freeze({ | ||
| MOVING_FORWARD: 4, | ||
| STOP: 3, | ||
| }); | ||
|
|
||
| export default TEST_CONFIG; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import RaceSimulator from '../models/RaceSimulator.js'; | ||
| import InputView from '../views/InputView.js'; | ||
| import OutputView from '../views/OutputView.js'; | ||
| import { validateNameListFormat, validateNumericValue } from '../utils/validators.js'; | ||
| import PROMPT_MESSAGES from '../constants/promptMessages.js'; | ||
|
|
||
| class RaceController { | ||
| #inputView; | ||
| #outputView; | ||
|
|
||
| constructor() { | ||
| this.#inputView = new InputView(); | ||
| this.#outputView = new OutputView(); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생성자를 통해 바로 값을 입력 받아서 넣는게 처리가 굉장히 깔끔한 것 같습니다. 저는 별도로 입력을 받은 후에 클래스를 선언하여 사용했는데 너무 좋은 방법 배워갑니다! |
||
|
|
||
| async start() { | ||
| const carNames = await this.#readCarNames(); | ||
| validateNameListFormat(carNames); | ||
| const attemptCount = await this.#readAttemptCount(); | ||
| validateNumericValue(attemptCount); | ||
| this.#runRace(carNames, Number(attemptCount)); | ||
| } | ||
|
|
||
| async #readCarNames() { | ||
| return this.#inputView.readString(PROMPT_MESSAGES.CAR_NAMES_INPUT); | ||
| } | ||
|
|
||
| async #readAttemptCount() { | ||
| return this.#inputView.readString(PROMPT_MESSAGES.ATTEMPT_COUNT_INPUT); | ||
| } | ||
|
|
||
| #printRaceProgress(simulator) { | ||
| this.#outputView.printResultHeader(); | ||
| while (simulator.hasRemainingAttempts()) { | ||
| const positions = simulator.executeStep(); | ||
| this.#outputView.printStepResult(positions); | ||
| } | ||
| } | ||
|
|
||
| #printFinalResults(simulator) { | ||
| this.#outputView.printWinners(simulator.getRaceWinners()); | ||
| } | ||
|
|
||
| #runRace(carNames, attemptCount) { | ||
| const simulator = new RaceSimulator(carNames, attemptCount); | ||
| this.#printRaceProgress(simulator); | ||
| this.#printFinalResults(simulator); | ||
| } | ||
|
Comment on lines
+44
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+31
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개인적으로는 runRace가 다른 두 메서드보다 먼저 선언되는 게 흐름 상 읽기 좋을 것 같습니당 |
||
| } | ||
|
|
||
| export default RaceController; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { validateNameLength } from '../utils/validators.js'; | ||
|
|
||
| class Car { | ||
| #name; | ||
| #position; | ||
|
|
||
| constructor(name) { | ||
| validateNameLength(name); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. validator를 생성자 안에 넣어 안전한 상태를 유지할 수도 있다는 걸 배워갑니다.. |
||
| this.#name = name; | ||
| this.#position = 0; | ||
| } | ||
|
|
||
| move() { | ||
| this.#position += 1; | ||
| } | ||
|
|
||
| getName() { | ||
| return this.#name; | ||
| } | ||
|
|
||
| getPosition() { | ||
| return this.#position; | ||
| } | ||
| } | ||
|
|
||
| export default Car; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
의존성이나 상태 관리, 확장성 관점에선 새로운 단일 인스턴스 생성을 constructor에서 하면 어떨까 생각했는데
어떻게 생각하시는지 궁금합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는
run()이 한 번 실행되고 종료되는 단일 실행 구조라RaceController를 재사용하거나 필드로 보관할 필요는 없다고 생각했습니다.constructor에서 생성하는 방식은 App 인스턴스가 여러 번 재사용되거나, 컨트롤러를 여러 메서드에서 공유해야 하거나, 의존성을 명시적으로 주입받아야 할 때 적합할 것 같습니다.
하지만 현재 구조에서는 App이 단순히 진입점 역할만 수행하고, 컨트롤러를 한 번만 사용하고 종료하기 때문에 지역 변수로 충분하다고 생각했습니다! 오히려 필드로 두면
왜 이게 필드인가?라는 의문이 생길 수 있을 것 같아요.RaceController내부에서도RaceSimulator를runRace()메서드 안에서 생성하는 식으로, 필요한 시점에 만드는 흐름을 전체적으로 맞췄습니다~