diff --git a/docs/README.md b/docs/README.md index 8ff1dce0a4..7fba4ed5bf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,3 +26,31 @@ - [x] 문구들을 어디서 출력할지. - [x] OutputView에서 printGameResultMessage 메소드는 적절한지? - [x] 유효성 검증을 도메인에서 할지, 입력받을 때 할지. + + +--- +#### 2단계 변경사항 +1. 상수 필드를 모두 static final로 변경. +2. 우승자 검출을 위한 자동차 비교 시 getter를 전혀 사용하지 않도록 수정. +3. 도메인과 컨트롤러 영역에서 View에 대해 전혀 모르도록 예외 메세지를 출력하는 부분까지 OutputView에게 위임함.
+4. 자동차 이름을 관리하는 VO를 생성했음. 자동차 이름에 대한 제약조건도 여럿 존재하고,
+이것을 자동차 객체가 다루기보다는 `carName` 객체가 다루는 것이 보다 적절한 책임 분배라고 생각했다.
+비슷한 이유로 `Position` VO를 분리함. +5. 시도 횟수를 다루는 객체를 둬서 프로덕션 및 테스트 코드를 간결하게 변경. + + +#### 2단계 고민사항 +1. 어느정도까지 확장성있게 설계하는게 옳을까? 얼마나 확장해야 오버프로그래밍이 되지 않을까?
+예를 들어서, OutputView가 지금은 `한국어로 콘솔에 출력`하는 것이지만, `영어로 콘솔에 출력` 된다거나, 콘솔이 아닌 다른 곳에 출력이 될 수도 있을 것이다.
+그러면 OutputView 위에 부모 클래스나 Interface를 정의하는 것이 낫지 않을까? 하지만 현재 문제 해결 상황에서는 전혀 필요하지 않다.
+오버 프로그래밍과 유연함 사이의 적정선은 어디인가?

+ +2. VO의 경우 메소드 및 프로퍼티의 네이밍은 어떻게 해야할까?
+CarName의 경우, 프로퍼티 및 getter의 이름은 어떻게 해야 좋을지 고민.
+프로퍼티 이름은 `carName`으로 했는데, 클래스명과 중복이 된다는 점에서 애매하다는 느낌을 받았음.
+또한 비슷한 이유에서 getter 이름을 `getCarName`이라고 하지 않고 `get` 이라고 정의했는데(carName.get() 처럼 사용될 수 있도록)
+괜찮은 방법인지도 고민!

+ +3. 현재 입력을 위한 출력이 InputView에서 수행되고 있는데,
+View가 콘솔창이 아니게 된다거나 하는 경우에는 InputView까지 수정이 되어야 한다.
+그러면 InputView에서도 출력이 필요할 때 OutputView를 호출해야 하지 않을까?
diff --git a/src/main/java/Application.java b/src/main/java/Application.java index c1b731359d..550db6c1f5 100644 --- a/src/main/java/Application.java +++ b/src/main/java/Application.java @@ -1,12 +1,12 @@ import controller.RacingController; import view.input.InputView; -import view.output.OutputView; +import view.output.ConsoleView; import java.util.Scanner; public class Application { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); - new RacingController(new InputView(scanner), new OutputView()).start(); + new RacingController(new InputView(scanner), new ConsoleView()).start(); } } diff --git a/src/main/java/controller/RacingController.java b/src/main/java/controller/RacingController.java index 15ea299a63..159a0c4d3a 100644 --- a/src/main/java/controller/RacingController.java +++ b/src/main/java/controller/RacingController.java @@ -3,64 +3,64 @@ import domain.RacingGame; import domain.RandomNumberGenerator; import view.input.InputView; -import view.output.OutputView; +import view.output.ConsoleView; import java.util.List; public class RacingController { private final InputView inputView; - private final OutputView outputView; + private final ConsoleView consoleView; private RacingGame racingGame; - public RacingController(InputView inputView, OutputView outputView) { + public RacingController(InputView inputView, ConsoleView consoleView) { this.inputView = inputView; - this.outputView = outputView; + this.consoleView = consoleView; } public void start() { try { - makeRacingGame(readCarNames()); - startRacingGame(readGameTry()); + makeRacingGame(readCarNames(), readGameTry()); + startRacingGame(); makeRacingGameResult(); - } catch (IllegalArgumentException e) { - System.out.println(e.getMessage()); + } catch (RuntimeException e) { + consoleView.printExceptionMessage(e.getMessage()); start(); } } - private void makeRacingGame(List carNames) { - this.racingGame = new RacingGame(carNames, new RandomNumberGenerator()); - } - private List readCarNames() { try { return inputView.readCarName(); } catch (IllegalArgumentException e) { - System.out.println(e.getMessage()); + consoleView.printExceptionMessage(e.getMessage()); return readCarNames(); } } - private void startRacingGame(int gameTry) { - outputView.printGameResultMessage(); - for (int i = 0; i < gameTry; i++) { - racingGame.start(); - outputView.printRacingStatus(racingGame.getCars()); - } - } - private int readGameTry() { try { return inputView.readGameTry(); } catch (IllegalArgumentException e) { - System.out.println(e.getMessage()); + consoleView.printExceptionMessage(e.getMessage()); return readGameTry(); } } + private void makeRacingGame(List carNames, int gameTry) { + this.racingGame = new RacingGame(carNames, gameTry, new RandomNumberGenerator()); + } + + private void startRacingGame() { + consoleView.printGameResultMessage(); + while (racingGame.isGameOnGoing()) { + racingGame.start(); + consoleView.printRacingStatus(racingGame.getCars()); + } + } + private void makeRacingGameResult() { - outputView.printRacingStatus(racingGame.getCars()); - outputView.printRacingWinners(racingGame.getWinners()); + consoleView.printRacingStatus(racingGame.getCars()); + consoleView.printRacingWinners(racingGame.getWinners()); } } diff --git a/src/main/java/domain/Car.java b/src/main/java/domain/Car.java index a14759df02..c05092ec72 100644 --- a/src/main/java/domain/Car.java +++ b/src/main/java/domain/Car.java @@ -2,47 +2,32 @@ public class Car { - private final String CAR_NAME_LENGTH_EXCEED = "[ERROR] 자동차 이름의 길이는 1자 이상, 5자 이하여야 합니다."; - private final String CAR_NAME_EMPTY = "[ERROR] 자동차의 이름은 공백이면 안됩니다."; + private final CarName carName; + private final Position position; - private final int MAX_CAR_NAME_LENGTH = 5; - - private int position; - private final String carName; - - public Car(String carName) { - validateCarName(carName); - this.carName = carName; - position = 0; - } - - public void validateCarName(String carName) { - validateCarNameIsNotEmpty(carName); - validateCarNameLength(carName); + public Car(String name, int position) { + this.carName = new CarName(name); + this.position = new Position(position); } - private void validateCarNameIsNotEmpty(String carName) { - if (carName.isBlank()) { - throw new IllegalArgumentException(CAR_NAME_EMPTY); - } + public void move() { + position.moveForward(); } - private void validateCarNameLength(String carName) { - if (carName.length() > MAX_CAR_NAME_LENGTH) { - throw new IllegalArgumentException(CAR_NAME_LENGTH_EXCEED); - } + public int comparePosition(Car otherCar) { + return this.getPosition() - otherCar.getPosition(); } - public void move() { - position++; + public boolean hasSamePositionWith(Car otherCar) { + return comparePosition(otherCar) == 0; } public int getPosition() { - return position; + return position.getPosition(); } public String getCarName() { - return carName; + return carName.getCarName(); } } diff --git a/src/main/java/domain/CarName.java b/src/main/java/domain/CarName.java new file mode 100644 index 0000000000..93c64decfd --- /dev/null +++ b/src/main/java/domain/CarName.java @@ -0,0 +1,37 @@ +package domain; + +import exception.CarNameBlankException; +import exception.CarNameLengthException; + +public class CarName { + + private static final int MAX_CAR_NAME_LENGTH = 5; + + private final String carName; + + public CarName(String carName) { + validateCarName(carName); + this.carName = carName; + } + + private void validateCarName(String carName) { + validateCarNameIsNotEmpty(carName); + validateCarNameLength(carName); + } + + private void validateCarNameIsNotEmpty(String carName) { + if (carName.isBlank()) { + throw new CarNameBlankException(); + } + } + + private void validateCarNameLength(String carName) { + if (carName.length() > MAX_CAR_NAME_LENGTH) { + throw new CarNameLengthException(); + } + } + + public String getCarName() { + return carName; + } +} diff --git a/src/main/java/domain/Coin.java b/src/main/java/domain/Coin.java new file mode 100644 index 0000000000..b49589135c --- /dev/null +++ b/src/main/java/domain/Coin.java @@ -0,0 +1,20 @@ +package domain; + +public class Coin { + + private int remaining; + + public Coin(int remaining) { + this.remaining = remaining; + } + + public void use() { + if (isLeft()) { + remaining--; + } + } + + public boolean isLeft() { + return remaining > 0; + } +} diff --git a/src/main/java/domain/Position.java b/src/main/java/domain/Position.java new file mode 100644 index 0000000000..92452f11c9 --- /dev/null +++ b/src/main/java/domain/Position.java @@ -0,0 +1,31 @@ +package domain; + +import exception.PositionInvalidException; + +public class Position { + + private int position; + + public Position(int position) { + validatePosition(position); + this.position = position; + } + + private void validatePosition(int position) { + validatePositionIsNotNegative(position); + } + + private void validatePositionIsNotNegative(int position) { + if (position < 0) { + throw new PositionInvalidException(); + } + } + + public void moveForward() { + position++; + } + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/domain/RacingGame.java b/src/main/java/domain/RacingGame.java index 723dea632d..fb8a879a74 100644 --- a/src/main/java/domain/RacingGame.java +++ b/src/main/java/domain/RacingGame.java @@ -1,5 +1,7 @@ package domain; +import exception.NoCarsExistException; + import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -7,16 +9,18 @@ public class RacingGame { - private final int MOVABLE_BOUND = 4; + private static final int DEFAULT_START_LINE = 0; + private static final int MOVABLE_BOUND = 4; private final List cars; private final NumberGenerator numberGenerator; + private final Coin gameCoin; - public RacingGame(List splitCarNames, NumberGenerator numberGenerator) { + public RacingGame(List splitCarNames, int gameTry, NumberGenerator numberGenerator) { cars = splitCarNames.stream() - .map(Car::new) + .map(carName -> new Car(carName, DEFAULT_START_LINE)) .collect(Collectors.toList()); - + gameCoin = new Coin(gameTry); this.numberGenerator = numberGenerator; } @@ -24,6 +28,7 @@ public void start() { for (Car car : cars) { moveCar(car); } + gameCoin.use(); } private void moveCar(Car car) { @@ -33,6 +38,10 @@ private void moveCar(Car car) { } } + public boolean isGameOnGoing() { + return gameCoin.isLeft(); + } + public List getCars() { return Collections.unmodifiableList(cars); } @@ -41,13 +50,13 @@ public List getWinners() { Car furthestCar = getFurthestCar(); return cars.stream() - .filter(car -> car.getPosition() == furthestCar.getPosition()) + .filter(car -> car.hasSamePositionWith(furthestCar)) .collect(Collectors.toList()); } private Car getFurthestCar() { return cars.stream() - .max(Comparator.comparingInt(Car::getPosition)) - .orElseThrow(RuntimeException::new); + .max(Car::comparePosition) + .orElseThrow(NoCarsExistException::new); } } diff --git a/src/main/java/domain/RandomNumberGenerator.java b/src/main/java/domain/RandomNumberGenerator.java index 6d7ba58b95..98f12f833a 100644 --- a/src/main/java/domain/RandomNumberGenerator.java +++ b/src/main/java/domain/RandomNumberGenerator.java @@ -4,7 +4,7 @@ public class RandomNumberGenerator implements NumberGenerator { - private final int DIGIT_MAX = 10; + private static final int DIGIT_MAX = 10; public int makeDigit() { Random random = new Random(); diff --git a/src/main/java/exception/CarNameBlankException.java b/src/main/java/exception/CarNameBlankException.java new file mode 100644 index 0000000000..652fcdb315 --- /dev/null +++ b/src/main/java/exception/CarNameBlankException.java @@ -0,0 +1,10 @@ +package exception; + +public class CarNameBlankException extends RuntimeException { + + private static final String MESSAGE = "[ERROR] 자동차의 이름은 공백이면 안됩니다."; + + public CarNameBlankException() { + super(MESSAGE); + } +} diff --git a/src/main/java/exception/CarNameLengthException.java b/src/main/java/exception/CarNameLengthException.java new file mode 100644 index 0000000000..640b38054f --- /dev/null +++ b/src/main/java/exception/CarNameLengthException.java @@ -0,0 +1,10 @@ +package exception; + +public class CarNameLengthException extends RuntimeException { + + private static final String MESSAGE = "[ERROR] 자동차 이름의 길이는 1자 이상, 5자 이하여야 합니다."; + + public CarNameLengthException() { + super(MESSAGE); + } +} diff --git a/src/main/java/exception/NoCarsExistException.java b/src/main/java/exception/NoCarsExistException.java new file mode 100644 index 0000000000..ee6adb8f3a --- /dev/null +++ b/src/main/java/exception/NoCarsExistException.java @@ -0,0 +1,10 @@ +package exception; + +public class NoCarsExistException extends RuntimeException { + + private final static String MESSAGE = "[ERROR] 자동차가 존재하지 않습니다."; + + public NoCarsExistException() { + super(MESSAGE); + } +} diff --git a/src/main/java/exception/PositionInvalidException.java b/src/main/java/exception/PositionInvalidException.java new file mode 100644 index 0000000000..d659ad7ef3 --- /dev/null +++ b/src/main/java/exception/PositionInvalidException.java @@ -0,0 +1,14 @@ +package exception; + +public class PositionInvalidException extends RuntimeException { + + private final static String MESSAGE = "[ERROR] 자동차의 위치 값은 음수일 수 없습니다."; + + public PositionInvalidException() { + super(MESSAGE); + } + + public String getKoreanMessage() { + return MESSAGE; + } +} diff --git a/src/main/java/view/input/InputValidator.java b/src/main/java/view/input/InputValidator.java index 7498e92d22..0253044ccb 100644 --- a/src/main/java/view/input/InputValidator.java +++ b/src/main/java/view/input/InputValidator.java @@ -2,17 +2,16 @@ public class InputValidator { - private final int MIN_GAME_TRY_COUNT = 1; - private final int MAX_GAME_TRY_COUNT = 10000; + private static final int MIN_GAME_TRY_RANGE = 1; + private static final int MAX_GAME_TRY_RANGE = 10000; - private final String GAME_TRY_COUNT_OUT_OF_RANGE = "[ERROR] 시도 횟수가 1번 이상 10000번 이하여야 합니다."; - private final String IS_NOT_INTEGER = "[ERROR] 정수 이외의 다른 문자를 입력해서는 안됩니다."; + private static final String GAME_TRY_COUNT_OUT_OF_RANGE = "[ERROR] 시도 횟수가 1번 이상 10000번 이하여야 합니다."; + private static final String IS_NOT_INTEGER = "[ERROR] 정수 이외의 다른 문자를 입력해서는 안됩니다."; - - public void validateGameTry(String gameTry) { + public void validateGameTryRange(String gameTry) { validateIsInteger(gameTry); - validateGameTryCount(Integer.parseInt(gameTry)); - } + validateGameTryRange(Integer.parseInt(gameTry)); + } private void validateIsInteger(String target) { try { @@ -21,9 +20,8 @@ private void validateIsInteger(String target) { throw new IllegalArgumentException(IS_NOT_INTEGER); } } - - private void validateGameTryCount(int gameTry) { - if (gameTry < MIN_GAME_TRY_COUNT || gameTry > MAX_GAME_TRY_COUNT) { + private void validateGameTryRange(int gameTry) { + if (gameTry < MIN_GAME_TRY_RANGE || gameTry > MAX_GAME_TRY_RANGE) { throw new IllegalArgumentException(GAME_TRY_COUNT_OUT_OF_RANGE); } } diff --git a/src/main/java/view/input/InputView.java b/src/main/java/view/input/InputView.java index 097aef2676..c9d43c8f17 100644 --- a/src/main/java/view/input/InputView.java +++ b/src/main/java/view/input/InputView.java @@ -5,8 +5,8 @@ public class InputView { - private final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; - private final String TRY_COUNT_INPUT_MESSAGE = "시도할 회수는 몇회인가요?"; + private static final String CAR_NAMES_INPUT_MESSAGE = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."; + private static final String TRY_COUNT_INPUT_MESSAGE = "시도할 회수는 몇회인가요?"; private final Scanner scanner; private final InputValidator inputValidator = new InputValidator(); @@ -29,7 +29,7 @@ public int readGameTry() { String gameTry = scanner.nextLine(); - inputValidator.validateGameTry(gameTry); + inputValidator.validateGameTryRange(gameTry); return Integer.parseInt(gameTry); } } diff --git a/src/main/java/view/output/OutputView.java b/src/main/java/view/output/ConsoleView.java similarity index 65% rename from src/main/java/view/output/OutputView.java rename to src/main/java/view/output/ConsoleView.java index 1fbe6ad1d7..af70a05aa6 100644 --- a/src/main/java/view/output/OutputView.java +++ b/src/main/java/view/output/ConsoleView.java @@ -5,13 +5,13 @@ import java.util.List; import java.util.StringJoiner; -public class OutputView { +public class ConsoleView { - private final String RACING_RESULT_MESSAGE = System.lineSeparator() + "실행 결과"; - private final String RACING_WINNER_MESSAGE = "%s가 최종 우승했습니다." + System.lineSeparator(); - private final String DISTANCE_MARK = "-"; - private final String CAR_INFO_DELIMITER = " : "; - private final String RESULT_DELIMITER = ", "; + private static final String RACING_RESULT_MESSAGE = System.lineSeparator() + "실행 결과"; + private static final String RACING_WINNER_MESSAGE = "%s가 최종 우승했습니다." + System.lineSeparator(); + private static final String DISTANCE_MARK = "-"; + private static final String CAR_INFO_DELIMITER = " : "; + private static final String RESULT_DELIMITER = ", "; public void printRacingStatus(List cars) { for (Car car : cars) { @@ -42,4 +42,8 @@ public void printRacingWinners(List cars) { public void printGameResultMessage() { System.out.println(RACING_RESULT_MESSAGE); } -} \ No newline at end of file + + public void printExceptionMessage(String exceptionMessage) { + System.out.println(exceptionMessage); + } +} diff --git a/src/test/java/domain/CarTest.java b/src/test/java/domain/CarTest.java index 6a04786bdf..aca2805682 100644 --- a/src/test/java/domain/CarTest.java +++ b/src/test/java/domain/CarTest.java @@ -15,7 +15,7 @@ class CarTest { @ParameterizedTest @CsvSource(value = {"3,3", "0,0", "213,213"}, delimiter = ',') void carMoveTest(int moveCount, int expectedPosition) { - Car car = new Car("dummy"); + Car car = new Car("dummy", 0); for (int i = 0; i < moveCount; i++) { car.move(); @@ -27,15 +27,14 @@ void carMoveTest(int moveCount, int expectedPosition) { @ParameterizedTest @ValueSource(strings = {" ", "", "\n", "abcdef"}) void validateCarNameWithFailureCaseTest(String carName) { - assertThatThrownBy(() -> new Car(carName)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new Car(carName, 0)) + .isInstanceOf(RuntimeException.class); } - @ParameterizedTest @ValueSource(strings = {"1", "a", "abc", "abcde"}) void validateCarNameWithSuccessCaseTest(String carName) { - assertThatCode(() -> new Car(carName)) + assertThatCode(() -> new Car(carName, 0)) .doesNotThrowAnyException(); } -} \ No newline at end of file +} diff --git a/src/test/java/domain/RacingGameTest.java b/src/test/java/domain/RacingGameTest.java index ac4c9b5e48..00c396a6a4 100644 --- a/src/test/java/domain/RacingGameTest.java +++ b/src/test/java/domain/RacingGameTest.java @@ -16,15 +16,12 @@ class RacingGameTest { @ParameterizedTest @MethodSource("parameterProvider") - void getWinnersTest(List carNames, List intendedNumbers, List expectedWinners) { + void getWinnersTest(List carNames, List intendedNumbers, int gameTry, List expectedWinners) { IntendedNumberGenerator intendedNumberGenerator = new IntendedNumberGenerator(); - RacingGame racingGame = new RacingGame( - carNames, - intendedNumberGenerator - ); + RacingGame racingGame = new RacingGame(carNames, gameTry, intendedNumberGenerator); intendedNumberGenerator.readRepository(intendedNumbers); - for (int i = 0; i < intendedNumbers.size() / carNames.size(); i++) { + while (racingGame.isGameOnGoing()) { racingGame.start(); } @@ -35,12 +32,12 @@ void getWinnersTest(List carNames, List intendedNumbers, List parameterProvider() { return Stream.of( - Arguments.of(List.of("pobi", "crong"), List.of(5, 3, 9, 0, 0, 9), List.of("pobi")), - Arguments.of(List.of("pobi", "crong"), List.of(3, 5, 9, 0, 0, 9), List.of("crong")), - Arguments.of(List.of("pobi", "crong"), List.of(4, 4, 4, 4, 4, 4), List.of("pobi", "crong")), - Arguments.of(List.of("pobi", "crong"), List.of(0, 0, 0, 0, 0, 0), List.of("pobi", "crong")), - Arguments.of(List.of("pobi", "crong", "hadi"), List.of(3, 4, 9, 6, 7, 6, 0, 1, 2), List.of("crong", "hadi")), - Arguments.of(List.of("pobi", "crong", "hadi"), List.of(7, 4, 9, 6, 7, 6, 0, 1, 2), List.of("pobi", "crong", "hadi")) + Arguments.of(List.of("pobi", "crong"), List.of(5, 3, 9, 0, 0, 9), 3, List.of("pobi")), + Arguments.of(List.of("pobi", "crong"), List.of(3, 5, 9, 0, 0, 9), 3, List.of("crong")), + Arguments.of(List.of("pobi", "crong"), List.of(4, 4, 4, 4, 4, 4), 3, List.of("pobi", "crong")), + Arguments.of(List.of("pobi", "crong"), List.of(0, 0, 0, 0, 0, 0), 3, List.of("pobi", "crong")), + Arguments.of(List.of("pobi", "crong", "hadi"), List.of(3, 4, 9, 6, 7, 6, 0, 1, 2), 3, List.of("crong", "hadi")), + Arguments.of(List.of("pobi", "crong", "hadi"), List.of(7, 4, 9, 6, 7, 6, 0, 1, 2), 3, List.of("pobi", "crong", "hadi")) ); } @@ -57,4 +54,4 @@ public int makeDigit() { return repository.get(index++); } } -} \ No newline at end of file +} diff --git a/src/test/java/view/input/InputValidatorTest.java b/src/test/java/view/input/InputValidatorTest.java index 18fdb9ac3b..2f7a8357ce 100644 --- a/src/test/java/view/input/InputValidatorTest.java +++ b/src/test/java/view/input/InputValidatorTest.java @@ -15,14 +15,14 @@ class InputValidatorTest { @ParameterizedTest @ValueSource(strings = {"0", "-1", "abc", " ", "10001", "3.5"}) void validateGameTryWithFailureCaseTest(String gameTry) { - assertThatThrownBy(() -> inputValidator.validateGameTry(gameTry)) + assertThatThrownBy(() -> inputValidator.validateGameTryRange(gameTry)) .isInstanceOf(IllegalArgumentException.class); } @ParameterizedTest @ValueSource(strings = {"1", "10000", "5000"}) void validateGameTryWithSuccessCaseTest(String gameTry) { - assertThatCode(() -> inputValidator.validateGameTry(gameTry)) + assertThatCode(() -> inputValidator.validateGameTryRange(gameTry)) .doesNotThrowAnyException(); } -} \ No newline at end of file +}