diff --git a/build.gradle b/build.gradle index a89989b..15f3352 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ dependencies { java { toolchain { - languageVersion = JavaLanguageVersion.of(21) + languageVersion = JavaLanguageVersion.of(17) } } diff --git a/docs/README.md b/docs/README.md index e69de29..511a891 100644 --- a/docs/README.md +++ b/docs/README.md @@ -0,0 +1,25 @@ +## 도메인 +### 모델 +- [X] RacingCar 클래스 + - [X] name 길이 검증 메서드 + - [X] name 존재 검증 메서드 + - [X] name 공백 검증 메서드 + - [X] position move 메서드 +- [X] RacingCar을 리스트로 한 RacingCars 클래스 + - [X] 이름 중복 검증 메서드 + - [X] 이름 목록 Empty 검증 메서드 + - [X] 레이스 참가 레이싱카 생성 팩토리 메서드 //이펙티브 자바 +- [X] 게임을 담당할 Race 클래스 생성 + - [X] 게임 시작 메서드 + - [X] position 비교 메서드 + - [X] 이곳에서 경기 결과 나오는게 맞다 판단하여 findResult, getWinner 메서드 생성 +- [X] 게임 시도 숫자 검증할 TryNumber 클래스 + + +## 컨트롤러 +- [X] 컨트롤러(컨트롤러 인터페이스화) +- +## View +- [X] view(인터페이스로 나누기 outView 재사용 위해) + - [X] 출력 되는 것들은 전부 outView 로 + - [X] 입력받는 것들은 전부 inputView 로 \ No newline at end of file diff --git a/src/main/java/racingcar/AppConfig.java b/src/main/java/racingcar/AppConfig.java new file mode 100644 index 0000000..682fd20 --- /dev/null +++ b/src/main/java/racingcar/AppConfig.java @@ -0,0 +1,22 @@ +package racingcar; + +import racingcar.controller.Controller; +import racingcar.controller.RaceController; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class AppConfig { + + public Controller controller() { + return new RaceController(inputView(),outputView()); + } + + private InputView inputView() { + return new InputView(); + } + private OutputView outputView() { + return new OutputView(); + } + + +} diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e..b88d408 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,13 @@ package racingcar; +import racingcar.controller.Controller; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + AppConfig appConfig = new AppConfig(); + + Controller controller = appConfig.controller(); + + controller.execute(); } } diff --git a/src/main/java/racingcar/controller/Controller.java b/src/main/java/racingcar/controller/Controller.java new file mode 100644 index 0000000..54a3960 --- /dev/null +++ b/src/main/java/racingcar/controller/Controller.java @@ -0,0 +1,5 @@ +package racingcar.controller; + +public interface Controller { + void execute(); +} diff --git a/src/main/java/racingcar/controller/RaceController.java b/src/main/java/racingcar/controller/RaceController.java new file mode 100644 index 0000000..d69e6b6 --- /dev/null +++ b/src/main/java/racingcar/controller/RaceController.java @@ -0,0 +1,73 @@ +package racingcar.controller; + +import racingcar.domain.model.*; +import racingcar.domain.model.dto.FinalWinnerDTO; +import racingcar.domain.model.dto.RoundResultDTO; +import racingcar.global.ErrorMessage; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class RaceController implements Controller { + public static final int START_ROUND = 1; + + private final InputView inputView; + private final OutputView outputView; + + public RaceController(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + @Override + public void execute() { + RacingCars racingCars = inputName(); + Race race = raceProcess(racingCars); + printResult(race); + } + + private void printResult(Race race) { + String finalResult = FinalWinnerDTO.from(race).getWinnersAsString(); + outputView.printWinningResult(finalResult); + } + + private Race raceProcess(RacingCars racingCars) { + outputView.printTryNumberNotice(); + TryNumber tryNumber = validateInputTryNumber(inputView.inputTryNumber()); + Race race = null; + for (int i = START_ROUND; i <= tryNumber.getTryNumber(); i++) { + race = Race.create(racingCars, tryNumber); + outputView.printRoundResult(RoundResultDTO.from(race.startRace(),race)); + } + return race; + } + + private RacingCars inputName() { + outputView.printInitialName(); + return RacingCars.create(createRacingCars()); + } + + + private List createRacingCars() { + return Arrays.stream(inputView.inputName().split(",")) + .map(String::trim) + .map(RacingCar::create) + .collect(Collectors.toList()); + } + + + private TryNumber validateInputTryNumber(String tryNumber) { + try { + if (tryNumber.trim().isEmpty()) { + throw new IllegalArgumentException(ErrorMessage.EMPTY_INPUT); + } + return TryNumber.from(Integer.parseInt(tryNumber)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ErrorMessage.INVALID_NUMBER_FORMAT); + } + + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/domain/model/Race.java b/src/main/java/racingcar/domain/model/Race.java new file mode 100644 index 0000000..3b6e7e0 --- /dev/null +++ b/src/main/java/racingcar/domain/model/Race.java @@ -0,0 +1,50 @@ +package racingcar.domain.model; + +import racingcar.global.ErrorMessage; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Race { + + private final RacingCars racingCars; + private final TryNumber tryNumber; + + private Race(RacingCars racingCars, TryNumber tryNumber) { + this.racingCars = racingCars; + this.tryNumber = tryNumber; + } + + public static Race create(RacingCars racingCars, TryNumber tryNumber) { + return new Race(racingCars, tryNumber); + } + + public RacingCars startRace() { + return RacingCars.create(racingCars.moveAll()); + } + + public Map findResult() { + return racingCars.getRacingCars().stream() + .collect(Collectors.toMap( + RacingCar::getName, + RacingCar::getPosition + )); + } + + public List getWinners() { + int winningPosition = findWinningPosition(); + return racingCars.getRacingCars().stream() + .filter(car -> car.getPosition() == winningPosition) + .map(RacingCar::getName) + .toList(); + } + + private int findWinningPosition() { + return racingCars.getRacingCars().stream() + .mapToInt(RacingCar::getPosition) + .max() + .orElseThrow(() -> new IllegalStateException(ErrorMessage.NO_CARS_TO_RACE)); + } +} + diff --git a/src/main/java/racingcar/domain/model/RacingCar.java b/src/main/java/racingcar/domain/model/RacingCar.java new file mode 100644 index 0000000..2735290 --- /dev/null +++ b/src/main/java/racingcar/domain/model/RacingCar.java @@ -0,0 +1,52 @@ +package racingcar.domain.model; + +import racingcar.global.ErrorMessage; + +import static camp.nextstep.edu.missionutils.Randoms.pickNumberInRange; + +public class RacingCar { + public static final int MIN = 0; + public static final int MAX = 9; + public static final int MOVE_NUMBER = 4; + public static int START_POSITION = 0; + + private final String name; + private int position = START_POSITION; + + private RacingCar(String name) { + validateNameLength(name); + validateNameEmpty(name); + this.name = name; + } + + public static RacingCar create(String name) { + return new RacingCar(name); + } + + private void validateNameLength(String name) { + if (name.length() > 5) { + throw new IllegalArgumentException(ErrorMessage.NAME_TOO_LONG); + } + } + + private void validateNameEmpty(String name) { + if (name == null) { + throw new IllegalArgumentException(ErrorMessage.NAME_MISSING); + } + } + + public void move() { + if (pickNumberInRange(MIN, MAX) >= MOVE_NUMBER) { + position++; + } + } + + public String getName() { + return name; + } + + + public int getPosition() { + return position; + } +} diff --git a/src/main/java/racingcar/domain/model/RacingCars.java b/src/main/java/racingcar/domain/model/RacingCars.java new file mode 100644 index 0000000..79236ee --- /dev/null +++ b/src/main/java/racingcar/domain/model/RacingCars.java @@ -0,0 +1,42 @@ +package racingcar.domain.model; + +import racingcar.global.ErrorMessage; + +import java.util.List; + +public class RacingCars { + private final List racingCars; + + private RacingCars(List racingCars) { + validateNameListEmpty(racingCars); + validateDuplicateName(racingCars); + this.racingCars = List.copyOf(racingCars); + } + + public static RacingCars create(List userInput) { + return new RacingCars(userInput); + } + + public List moveAll() { + racingCars.forEach(RacingCar::move); + return racingCars; + } + + + private void validateDuplicateName(List racingCars) { + if (racingCars.size() != racingCars.stream().distinct().count()) { + throw new IllegalArgumentException(ErrorMessage.DUPLICATE_NAME); + } + } + + private void validateNameListEmpty(List racingCars) { + if (racingCars.isEmpty()) { + throw new IllegalArgumentException(ErrorMessage.EMPTY_NAME_LIST); + } + } + + + public List getRacingCars() { + return racingCars; + } +} diff --git a/src/main/java/racingcar/domain/model/TryNumber.java b/src/main/java/racingcar/domain/model/TryNumber.java new file mode 100644 index 0000000..71b1a7f --- /dev/null +++ b/src/main/java/racingcar/domain/model/TryNumber.java @@ -0,0 +1,29 @@ +package racingcar.domain.model; + +import racingcar.global.ErrorMessage; + +public class TryNumber { + public static final int MIN = 0; + + + private final int tryNumber; + + private TryNumber(int tryNumber) { + validateTryNumber(tryNumber); + this.tryNumber = tryNumber; + } + + public static TryNumber from(int tryNumber) { + return new TryNumber(tryNumber); + } + + private void validateTryNumber(int tryNumber) { + if (tryNumber <= MIN) { + throw new IllegalArgumentException(ErrorMessage.INVALID_TRY_NUMBER); + } + } + + public int getTryNumber() { + return tryNumber; + } +} diff --git a/src/main/java/racingcar/domain/model/dto/FinalWinnerDTO.java b/src/main/java/racingcar/domain/model/dto/FinalWinnerDTO.java new file mode 100644 index 0000000..35b6961 --- /dev/null +++ b/src/main/java/racingcar/domain/model/dto/FinalWinnerDTO.java @@ -0,0 +1,22 @@ +package racingcar.domain.model.dto; + +import racingcar.domain.model.Race; + + +public class FinalWinnerDTO { + private final Race race; + + private FinalWinnerDTO(Race race) { + this.race = race; + } + + public static FinalWinnerDTO from(Race race) { + return new FinalWinnerDTO(race); + } + + public String getWinnersAsString() { + return String.join(", ", race.getWinners()); + } + + +} diff --git a/src/main/java/racingcar/domain/model/dto/RoundResultDTO.java b/src/main/java/racingcar/domain/model/dto/RoundResultDTO.java new file mode 100644 index 0000000..6705606 --- /dev/null +++ b/src/main/java/racingcar/domain/model/dto/RoundResultDTO.java @@ -0,0 +1,26 @@ +package racingcar.domain.model.dto; + +import racingcar.domain.model.Race; +import racingcar.domain.model.RacingCars; + +import java.util.Map; + +public class RoundResultDTO { + private final RacingCars racingCars; + private final Race race; + + private RoundResultDTO(RacingCars racingCars, Race race) { + this.racingCars = racingCars; + this.race = race; + } + + + public static RoundResultDTO from(RacingCars racingCars, Race race) { + return new RoundResultDTO(racingCars, race); + } + + + public Map getRacingCars() { + return race.findResult(); + } +} diff --git a/src/main/java/racingcar/global/ErrorMessage.java b/src/main/java/racingcar/global/ErrorMessage.java new file mode 100644 index 0000000..1636484 --- /dev/null +++ b/src/main/java/racingcar/global/ErrorMessage.java @@ -0,0 +1,14 @@ +package racingcar.global; + +public class ErrorMessage { + public static final String EMPTY_INPUT = "빈 값이 들어오면 안됩니다."; + public static final String INVALID_NUMBER_FORMAT = "정수만 입력 가능합니다."; + + public static final String NAME_TOO_LONG = "이름은 5자 이하여야 합니다."; + public static final String NAME_MISSING = "이름이 존재하지 않습니다."; + public static final String DUPLICATE_NAME = "중복된 이름이 존재합니다."; + public static final String EMPTY_NAME_LIST = "이름 목록이 비어있습니다."; + + public static final String NO_CARS_TO_RACE = "경주할 자동차가 없습니다."; + public static final String INVALID_TRY_NUMBER = "시도 숫자는 0보다 커야합니다."; +} diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000..499e4aa --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,14 @@ +package racingcar.view; + +import static camp.nextstep.edu.missionutils.Console.readLine; + +public class InputView { + public String inputName() { + return readLine(); + } + + public String inputTryNumber() { + return readLine(); + } + +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000..1b4608f --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,28 @@ +package racingcar.view; + +import racingcar.domain.model.dto.RoundResultDTO; + +public class OutputView { + + public void printInitialName() { + System.out.println("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"); + } + + + public void printRoundResult(RoundResultDTO roundResultDTO) { + roundResultDTO.getRacingCars().forEach((name, position) -> + System.out.println(name + " : " + "-".repeat(position)) + ); + System.out.println(); + } + + + public void printTryNumberNotice() { + System.out.println("시도할 회수는 몇회인가요?"); + } + + public void printWinningResult(String result) { + System.out.println("최종 우승자 : " + result); + } + +} diff --git a/src/test/java/racingcar/controller/RaceControllerTest.java b/src/test/java/racingcar/controller/RaceControllerTest.java new file mode 100644 index 0000000..039a940 --- /dev/null +++ b/src/test/java/racingcar/controller/RaceControllerTest.java @@ -0,0 +1,26 @@ +package racingcar.controller; + +import org.junit.jupiter.api.Test; +import racingcar.domain.model.Race; + +import racingcar.view.InputView; +import racingcar.view.OutputView; + + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RaceControllerTest { +// void 문자가_입력되면_예외가_발생한다() { +// // given +// Race mockRace = mock(Race.class); +// InputView mockInputView = mock(InputView.class); +// OutputView mockOutputView = mock(OutputView.class); +// when(mockInputView.inputTryNumber()).thenReturn("1"); +// RaceController controller = new RaceController(); +// +// assertThrows(IllegalArgumentException.class, controller::execute); +// } +} \ No newline at end of file diff --git a/src/test/java/racingcar/domain/RacingCarTest.java b/src/test/java/racingcar/domain/RacingCarTest.java new file mode 100644 index 0000000..af3ccee --- /dev/null +++ b/src/test/java/racingcar/domain/RacingCarTest.java @@ -0,0 +1,17 @@ +package racingcar.domain; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import racingcar.domain.model.RacingCar; + +class RacingCarTest { + + @Test + void validateName() { + } + + @Test + void move() { + + } +} \ No newline at end of file