-
Notifications
You must be signed in to change notification settings - Fork 907
[자동차 경주] 이선욱 미션 제출합니다. #923
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
base: main
Are you sure you want to change the base?
Changes from all commits
361cbb7
a6913f9
8704960
f6e8f0b
d4e7058
c7e2ea1
6056a21
f8c9348
7478b65
49b3342
0084494
48b95f5
61e8be3
f8ed21d
97fed3b
21cc2f5
3d4d054
0a93e13
0ecfc06
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,47 @@ | ||
| # java-racingcar-precourse | ||
| ## 기능 구현 목록 | ||
|
|
||
| ### 1. 입력 기능 | ||
| - [x] 자동차 이름 입력 받기 | ||
| - [x] 시도 횟수 입력 받기 | ||
|
|
||
| ### 2. 입력 검증 기능 | ||
| **자동차 이름 검증** | ||
| - [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 | ||
| - [x] 자동차 이름이 5자를 초과하는 경우 예외 처리 | ||
| - [x] 자동차 이름이 빈 문자열인 경우 예외 처리 | ||
| - [x] 자동차 이름이 공백만 있는 경우 예외 처리 | ||
|
|
||
| **시도 횟수 검증** | ||
| - [x] 입력값이 null이거나 빈 문자열인 경우 예외 처리 | ||
| - [x] 숫자가 아닌 값 입력 시 예외 처리 | ||
| - [x] 0 이하의 값 입력 시 예외 처리 | ||
| - [x] 정수 범위를 벗어나는 값 입력 시 예외 처리 | ||
|
|
||
| ### 3. 입력 파싱 기능 | ||
| - [x] 쉼표(,)를 기준으로 자동차 이름 분리 | ||
| - [x] 입력받은 시도 횟수를 정수로 변환 | ||
|
|
||
| ### 4. 자동차 기능 | ||
| - [x] 자동차 객체 생성 (이름 저장) | ||
| - [x] 자동차의 현재 위치 저장 | ||
| - [x] 자동차 전진 기능 | ||
| - [x] 자동차의 현재 위치 조회 | ||
|
|
||
| ### 5. 게임 진행 기능 | ||
| - [x] 게임 라운드 반복 실행 (n회) | ||
| - [x] 각 라운드마다 모든 자동차에 대해 이동 시도 | ||
| - [x] 0~9 사이의 무작위 값 생성 | ||
| - [x] 무작위 값이 4 이상일 경우 자동차 전진 | ||
|
|
||
| ### 6. 우승자 판별 기능 | ||
| - [x] 모든 자동차 중 최대 이동 거리 찾기 | ||
| - [x] 최대 이동 거리를 가진 자동차들을 우승자로 선정 | ||
|
|
||
| ### 7. 출력 기능 | ||
| - [x] 각 라운드 후 모든 자동차의 이름과 위치 출력 | ||
| - [x] 자동차 위치를 `-` 기호로 표시 | ||
| - [x] 우승자 이름 출력 (단독 우승자) | ||
| - [x] 우승자 이름 출력 (공동 우승자, 쉼표로 구분) | ||
|
|
||
| --- | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,10 @@ | ||
| package racingcar; | ||
|
|
||
| import racingcar.controller.RacingGameController; | ||
|
|
||
| public class Application { | ||
| public static void main(String[] args) { | ||
| // TODO: 프로그램 구현 | ||
| RacingGameController controller = new RacingGameController(); | ||
| controller.run(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| package racingcar.controller; | ||
|
|
||
| import racingcar.domain.Car; | ||
| import racingcar.domain.RacingGame; | ||
| import racingcar.validator.InputValidator; | ||
| import racingcar.view.InputView; | ||
| import racingcar.view.OutputView; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class RacingGameController { | ||
|
|
||
| public void run() { | ||
| List<String> carNames = readAndValidateCarNames(); | ||
| int roundCount = readAndValidateRoundCount(); | ||
|
|
||
| RacingGame game = new RacingGame(carNames, roundCount); | ||
|
|
||
| OutputView.printResultMessage(); | ||
| playGameWithOutput(game, roundCount); | ||
|
|
||
| List<String> winners = game.getWinners(); | ||
| OutputView.printWinners(winners); | ||
| } | ||
|
|
||
| private List<String> readAndValidateCarNames() { | ||
| List<String> carNames = InputView.readCarNames(); | ||
| String input = String.join(",", carNames); | ||
| InputValidator.validateCarNames(input); | ||
| return carNames; | ||
| } | ||
|
|
||
| private int readAndValidateRoundCount() { | ||
| int roundCount = InputView.readRoundCount(); | ||
| InputValidator.validateRoundCount(String.valueOf(roundCount)); | ||
| return roundCount; | ||
| } | ||
|
Comment on lines
+33
to
+37
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. 구조상 NumberFormatException이 날수도 있을것 같은데 readRountCount 하기전에 먼저 검증을 하셔야 오류를 피할수 있을것 같습니당!
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. 넵 감사합니다! |
||
|
|
||
| private void playGameWithOutput(RacingGame game, int roundCount) { | ||
| for (int i = 0; i < roundCount; i++) { | ||
| game.playRound(); | ||
| printRoundResult(game); | ||
| } | ||
| } | ||
|
Comment on lines
+39
to
+44
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. 이 부분 메서드 작명에서 의도를 드러내신 부분 정말 잘하신 것 같아요! 👍 많은 분들이 playGame() 이렇게 작성하셨는데, Controller 본연의 역할(model과 view의 오케스트레이션)이 추상화되어 문제가 있다고 보였습니다. 선욱님은 그 부분을 명시해주신 부분이 인상깊어요 |
||
|
|
||
| private void printRoundResult(RacingGame game) { | ||
| List<Car> cars = game.getCars(); | ||
| for (Car car : cars) { | ||
| OutputView.printCarStatus(car.getName(), car.getPosition()); | ||
| } | ||
|
Comment on lines
+48
to
+50
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. stream Api를 쓰는 것도 좋을 것 같습니다. |
||
| OutputView.printRoundResult(); | ||
| } | ||
| } | ||
|
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. EOF 신경쓰면 좋을 것 같아요 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. RacingGame의 로그를 찍는 느낌이라 RacingGame내부에 작성하는건 어떤가요??
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. 말씀하신대로 RacingGame에서 하는게 역할이 더 맞는거 같네요 감사합니다! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package racingcar.domain; | ||
|
|
||
| public class Car { | ||
| private static final int MAX_NAME_LENGTH = 5; | ||
| private static final int INITIAL_POSITION = 0; | ||
|
|
||
| private final String name; | ||
| private int position; | ||
|
|
||
| public Car(String name) { | ||
| validateName(name); | ||
| this.name = name; | ||
| this.position = INITIAL_POSITION; | ||
| } | ||
|
|
||
| private void validateName(String name) { | ||
| if (name == null || name.isEmpty()) { | ||
| throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); | ||
| } | ||
| if (name.isBlank()) { | ||
| throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); | ||
| } | ||
| if (name.length() > MAX_NAME_LENGTH) { | ||
| throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); | ||
|
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. 5자 이하에서 "5"도 상수로 구성시키면 좋을 것 같아요. 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. DRY! 좋은 피드백인 것 같아요! |
||
| } | ||
| } | ||
|
|
||
| public String getName() { | ||
| return name; | ||
| } | ||
|
|
||
| public int getPosition() { | ||
| return position; | ||
| } | ||
|
|
||
| public void moveForward() { | ||
| position++; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package racingcar.domain; | ||
|
|
||
| import camp.nextstep.edu.missionutils.Randoms; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class RacingGame { | ||
| private static final int RANDOM_NUMBER_MIN = 0; | ||
| private static final int RANDOM_NUMBER_MAX = 9; | ||
| private static final int MOVE_FORWARD_THRESHOLD = 4; | ||
|
|
||
| private final List<Car> cars; | ||
| private final int rounds; | ||
|
|
||
| public RacingGame(List<String> carNames, int rounds) { | ||
| this.cars = carNames.stream() | ||
| .map(Car::new) | ||
| .collect(Collectors.toList()); | ||
|
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. 자바 17부턴 .toList()만 사용하셔도 됩니다!
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. 옛날 자료를 보고 공부했더니 몰랐네요 감사합니다! 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. 현재 문법과 .toList()의 차이점이 있으니 사용하기 전에 한번 확인해보시면 좋을 것 같아요! |
||
| this.rounds = rounds; | ||
| } | ||
|
|
||
| public List<Car> getCars() { | ||
|
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. 다른사람들의 코드를 보고 스스로 개선할점 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. 방어적 복사 좋습니다 :) |
||
| return new ArrayList<>(cars); | ||
| } | ||
|
|
||
| public void play() { | ||
| for (int i = 0; i < rounds; i++) { | ||
| playRound(); | ||
| } | ||
| } | ||
|
Comment on lines
+28
to
+32
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. 이 메소드는 테스트를 위한 메소드로 확인이 되는데, |
||
|
|
||
| public void playRound() { | ||
| for (Car car : cars) { | ||
| tryMove(car); | ||
| } | ||
| } | ||
|
|
||
| private void tryMove(Car car) { | ||
|
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. 도메인 주도 설계적으로 생각해봤을 때, 이 함수는 Car 안으로 들어가는게 나아보여요.
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. 피드백 감사합니다! |
||
| int randomNumber = Randoms.pickNumberInRange(RANDOM_NUMBER_MIN, RANDOM_NUMBER_MAX); | ||
| if (randomNumber >= MOVE_FORWARD_THRESHOLD) { | ||
| car.moveForward(); | ||
| } | ||
| } | ||
|
|
||
| public List<String> getWinners() { | ||
| int maxPosition = findMaxPosition(); | ||
| return cars.stream() | ||
| .filter(car -> car.getPosition() == maxPosition) | ||
| .map(Car::getName) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private int findMaxPosition() { | ||
| return cars.stream() | ||
| .mapToInt(Car::getPosition) | ||
| .max() | ||
| .orElse(0); | ||
| } | ||
| } | ||
|
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,52 @@ | ||
| package racingcar.validator; | ||
|
|
||
| public class InputValidator { | ||
| private static final int MAX_NAME_LENGTH = 5; | ||
|
|
||
| private InputValidator() { | ||
| } | ||
|
|
||
| public static void validateCarNames(String carNames) { | ||
| if (carNames == null || carNames.isEmpty()) { | ||
| throw new IllegalArgumentException("자동차 이름은 null이거나 빈 문자열일 수 없습니다."); | ||
| } | ||
|
|
||
| String[] names = carNames.split(",", -1); | ||
| for (String name : names) { | ||
| validateSingleCarName(name.trim()); | ||
| } | ||
| } | ||
|
|
||
| private static void validateSingleCarName(String name) { | ||
| if (name.isEmpty()) { | ||
| throw new IllegalArgumentException("자동차 이름은 빈 문자열일 수 없습니다."); | ||
| } | ||
| if (name.isBlank()) { | ||
| throw new IllegalArgumentException("자동차 이름은 공백만으로 이루어질 수 없습니다."); | ||
| } | ||
| if (name.length() > MAX_NAME_LENGTH) { | ||
| throw new IllegalArgumentException("자동차 이름은 5자 이하여야 합니다."); | ||
| } | ||
| } | ||
|
Comment on lines
+20
to
+30
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. Car 객체 생성자에서 검증하는 것과 동일한 코드인데, Car 클래스 내부 검증이 중요하므로 검증 로직을 Car 생성자에 넣은 것으로 이해했습니다. 다만, 이후에 규칙을 바꾸는 경우 두 곳 모두를 변경해야 하므로 유지보수에서 문제가 발생할 것 같습니다. 그리고 문자열 입력값 자체를 검증하는 기능도 넣어주셨는데 저는 이 부분을 깜빡했네요. 꼼꼼하게 예외처리하신 것 배워갑니다. 추가로 isBlank()에서도 isEmpty()와 같이 빈 문자열일때 true를 반환하지만, 빈 문자열에 대한 예외를 강조하기 위해서 따로 조건문을 두신 것으로 이해했습니다.
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. 입력시에도 검증해야한다고 생각해 이렇게 작성했는데 말씀해주신 부분 생각해보니 코드 중복이 너무 심해질 수 있을거 같네요. 저도 객체에서만 검증하도록 하는것이 더 좋을거 같다는 생각이 듭니다. isBlank는 말씀하신대로 두 경우의 차이에서 의도를 나타내기 위해 추가했는데 괜찮은 선택인지는 잘 모르겠네요 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. Fast Fail을 고려하신 것 같아서 전 보기 좋습니다! |
||
|
|
||
| public static void validateRoundCount(String roundCount) { | ||
| if (roundCount == null || roundCount.isEmpty()) { | ||
| throw new IllegalArgumentException("시도 횟수는 null이거나 빈 문자열일 수 없습니다."); | ||
| } | ||
|
|
||
| if (roundCount.isBlank()) { | ||
| throw new IllegalArgumentException("시도 횟수는 공백일 수 없습니다."); | ||
| } | ||
|
|
||
| int count; | ||
|
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. 선언을 try 바깥에서 한 이유가 있을까요??
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. 아마 밑에 if문에서도 사용하니까 try문 내용과 구분하려고 바깥에서 선언을 한거 같은데 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. 코드 가독성이 더 좋아질 것 같아요 ㅎㅎ |
||
| try { | ||
| count = Integer.parseInt(roundCount); | ||
| } catch (NumberFormatException e) { | ||
| throw new IllegalArgumentException("시도 횟수는 숫자여야 합니다."); | ||
| } | ||
|
|
||
| if (count <= 0) { | ||
| throw new IllegalArgumentException("시도 횟수는 0보다 커야 합니다."); | ||
|
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. 3주차부터는 enum을 사용하라고 하셨으니까 예외 메세지를 모아놓은 enum을 쓰면 좋을 것 같아요.
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. 감사합니다! enum과 상수를 사용하도록 신경써야겠네요 |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package racingcar.view; | ||
|
|
||
| import camp.nextstep.edu.missionutils.Console; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class InputView { | ||
| private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; | ||
| private static final String ROUND_COUNT_INPUT_MESSAGE = "시도할 횟수는 몇 회인가요?"; | ||
|
|
||
| private InputView() { | ||
| } | ||
|
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. 이런 static메서드를 사용할경우에는 생성자를 숨기는게 좋네요 하나 배워갑니다 |
||
|
|
||
| public static List<String> readCarNames() { | ||
| System.out.println(CAR_NAMES_INPUT_MESSAGE); | ||
| String input = Console.readLine(); | ||
|
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. 우테코에서 제공하는 missionutils 라이브러리 안에 Console.close() 함수가 있다고 하더라구요. 입력이 다 끝난 뒤 혹은 main 마지막에 이 함수를 쓰는게 좋아보여요. 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. Console.close()는 JVM이 inputStream을 알아서 처리해주기 때문에, 명시적으로 표현하지 않아도 될 것 같아요! |
||
| return parseCarNames(input); | ||
| } | ||
|
|
||
| public static List<String> parseCarNames(String input) { | ||
| return Arrays.stream(input.split(",")) | ||
| .map(String::trim) | ||
| .collect(Collectors.toList()); | ||
| } | ||
|
|
||
| public static int readRoundCount() { | ||
| System.out.println(ROUND_COUNT_INPUT_MESSAGE); | ||
| String input = Console.readLine(); | ||
| return parseRoundCount(input); | ||
| } | ||
|
|
||
| public static int parseRoundCount(String input) { | ||
| return Integer.parseInt(input); | ||
| } | ||
|
Comment on lines
+34
to
+36
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. 파싱하기 전에 검증이 필요해보입니다! |
||
| } | ||
|
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,40 @@ | ||
| package racingcar.view; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| public class OutputView { | ||
| private static final String RESULT_MESSAGE = "실행 결과"; | ||
| private static final String WINNER_PREFIX = "최종 우승자 : "; | ||
| private static final String POSITION_SYMBOL = "-"; | ||
| private static final String CAR_STATUS_FORMAT = "%s : %s"; | ||
|
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. 출력 형식을 포맷 문자열로 정할수 있는거 잊고있었네요. |
||
| private static final String WINNER_DELIMITER = ", "; | ||
|
|
||
| private OutputView() { | ||
| } | ||
|
|
||
| public static void printResultMessage() { | ||
| System.out.println(); | ||
| System.out.println(RESULT_MESSAGE); | ||
| } | ||
|
|
||
| public static void printCarStatus(String carName, int position) { | ||
| System.out.println(formatCarStatus(carName, position)); | ||
| } | ||
|
|
||
| public static String formatCarStatus(String carName, int position) { | ||
| String positionDisplay = POSITION_SYMBOL.repeat(position); | ||
| return String.format(CAR_STATUS_FORMAT, carName, positionDisplay); | ||
| } | ||
|
|
||
| public static void printRoundResult() { | ||
| System.out.println(); | ||
| } | ||
|
|
||
| public static void printWinners(List<String> winners) { | ||
| System.out.println(formatWinners(winners)); | ||
| } | ||
|
|
||
| public static String formatWinners(List<String> winners) { | ||
| return WINNER_PREFIX + String.join(WINNER_DELIMITER, winners); | ||
| } | ||
| } | ||
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.
개인적으론 메서드 이름에 and를 넣는건 지양하는 편입니다.
두가지 일을 동시에 한다는 인상을 주기에 충분하기 때문입니다!
또한 validate의 일은 이미 Validator에게 넘기셔서 read의 동작만 나타내는 네이밍을 하셔도 괜찮지 않을까 싶습니다~
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.
그렇군요
감사합니다! 확실히 하나의 확실한 네이밍이 더 직관적인거 같네요
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.
저도 and 주의해야겠어요. 감사합니다