From 295b5d1a10a02ec0c1297fe47e53924041019ea5 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 11:33:29 +0900 Subject: [PATCH 01/10] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=AA=A9=EB=A1=9D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 189 ++++++++---------------------------------------------- 1 file changed, 26 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 5c937d3..b073247 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,26 @@ -# 미션 - 자동차 경주 - -## 🔍 진행 방식 - -- 미션은 **기능 요구 사항, 프로그래밍 요구 사항, 과제 진행 요구 사항** 세 가지로 구성되어 있다. -- 세 개의 요구 사항을 만족하기 위해 노력한다. 특히 기능을 구현하기 전에 기능 목록을 만들고, 기능 단위로 커밋 하는 방식으로 진행한다. -- 기능 요구 사항에 기재되지 않은 내용은 스스로 판단하여 구현한다. - -## 📮 미션 제출 방법 - -- 미션 구현을 완료한 후 GitHub을 통해 제출해야 한다. - - GitHub을 활용한 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고해 제출한다. -- GitHub에 미션을 제출한 후 [우아한테크코스 지원](https://apply.techcourse.co.kr) 사이트에 접속하여 프리코스 과제를 제출한다. - - 자세한 방법은 [제출 가이드](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse#제출-가이드) 참고 - - **Pull Request만 보내고 지원 플랫폼에서 과제를 제출하지 않으면 최종 제출하지 않은 것으로 처리되니 주의한다.** - -## 🚨 과제 제출 전 체크 리스트 - 0점 방지 - -- 기능 구현을 모두 정상적으로 했더라도 **요구 사항에 명시된 출력값 형식을 지키지 않을 경우 0점으로 처리**한다. -- 기능 구현을 완료한 뒤 아래 가이드에 따라 테스트를 실행했을 때 모든 테스트가 성공하는지 확인한다. -- **테스트가 실패할 경우 0점으로 처리**되므로, 반드시 확인 후 제출한다. - -### 테스트 실행 가이드 - -- 터미널에서 `java -version`을 실행하여 Java 버전이 17인지 확인한다. - Eclipse 또는 IntelliJ IDEA와 같은 IDE에서 Java 17로 실행되는지 확인한다. -- 터미널에서 Mac 또는 Linux 사용자의 경우 `./gradlew clean test` 명령을 실행하고, - Windows 사용자의 경우 `gradlew.bat clean test` 또는 `./gradlew.bat clean test` 명령을 실행할 때 모든 테스트가 아래와 같이 통과하는지 확인한다. - -``` -BUILD SUCCESSFUL in 0s -``` - ---- - -## 🚀 기능 요구 사항 - -초간단 자동차 경주 게임을 구현한다. - -- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. -- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. -- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. -- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. -- 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다. -- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다. -- 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분한다. -- 사용자가 잘못된 값을 입력할 경우 `IllegalArgumentException`을 발생시킨 후 애플리케이션은 종료되어야 한다. - -### 입출력 요구 사항 - -#### 입력 - -- 경주 할 자동차 이름(이름은 쉼표(,) 기준으로 구분) - -``` -pobi,woni,jun -``` - -- 시도할 회수 - -``` -5 -``` - -#### 출력 - -- 각 차수별 실행 결과 - -``` -pobi : -- -woni : ---- -jun : --- -``` - -- 단독 우승자 안내 문구 - -``` -최종 우승자 : pobi -``` - -- 공동 우승자 안내 문구 - -``` -최종 우승자 : pobi, jun -``` - -#### 실행 결과 예시 - -``` -경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분) -pobi,woni,jun -시도할 회수는 몇회인가요? -5 - -실행 결과 -pobi : - -woni : -jun : - - -pobi : -- -woni : - -jun : -- - -pobi : --- -woni : -- -jun : --- - -pobi : ---- -woni : --- -jun : ---- - -pobi : ----- -woni : ---- -jun : ----- - -최종 우승자 : pobi, jun -``` - ---- - -## 🎯 프로그래밍 요구 사항 - -- JDK 17 버전에서 실행 가능해야 한다. **JDK 17에서 정상적으로 동작하지 않을 경우 0점 처리한다.** -- 프로그램 실행의 시작점은 `Application`의 `main()`이다. -- `build.gradle` 파일을 변경할 수 없고, 외부 라이브러리를 사용하지 않는다. -- [Java 코드 컨벤션](https://github.com/woowacourse/woowacourse-docs/tree/master/styleguide/java) 가이드를 준수하며 프로그래밍한다. -- 프로그램 종료 시 `System.exit()`를 호출하지 않는다. -- 프로그램 구현이 완료되면 `ApplicationTest`의 모든 테스트가 성공해야 한다. **테스트가 실패할 경우 0점 처리한다.** -- 프로그래밍 요구 사항에서 달리 명시하지 않는 한 파일, 패키지 이름을 수정하거나 이동하지 않는다. - -### 추가된 요구 사항 - -- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다. - - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. - - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. -- 3항 연산자를 쓰지 않는다. -- 함수(또는 메서드)가 한 가지 일만 하도록 최대한 작게 만들어라. -- JUnit 5와 AssertJ를 이용하여 본인이 정리한 기능 목록이 정상 동작함을 테스트 코드로 확인한다. - - 테스트 도구 사용법이 익숙하지 않다면 `test/java/study`를 참고하여 학습한 후 테스트를 구현한다. - -### 라이브러리 - -- JDK에서 제공하는 Random 및 Scanner API 대신 `camp.nextstep.edu.missionutils`에서 제공하는 `Randoms` 및 `Console` API를 사용하여 구현해야 한다. - - Random 값 추출은 `camp.nextstep.edu.missionutils.Randoms`의 `pickNumberInRange()`를 활용한다. - - 사용자가 입력하는 값은 `camp.nextstep.edu.missionutils.Console`의 `readLine()`을 활용한다. - -#### 사용 예시 - -- 0에서 9까지의 정수 중 한 개의 정수 반환 - -```java -Randoms.pickNumberInRange(0,9); -``` - ---- - -## ✏️ 과제 진행 요구 사항 - -- 미션은 [java-racingcar-6](https://github.com/woowacourse-precourse/java-racingcar-6) 저장소를 Fork & Clone해 시작한다. -- **기능을 구현하기 전 `docs/README.md`에 구현할 기능 목록을 정리**해 추가한다. -- **Git의 커밋 단위는 앞 단계에서 `docs/README.md`에 정리한 기능 목록 단위**로 추가한다. - - [커밋 메시지 컨벤션](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 가이드를 참고해 커밋 메시지를 작성한다. -- 과제 진행 및 제출 방법은 [프리코스 과제 제출](https://github.com/woowacourse/woowacourse-docs/tree/master/precourse) 문서를 참고한다. +### 기능구현 목록 + +- [ ] 경주할 자동차들의 이름과 시도할 횟수를 입력받아 저장한다. + - [ ] 유효성 검사와 예외 처리를 통해 올바른 입력만 허용한다. (예: 빈 입력, 중복된 이름, 자동차 이름 길이 등) +- [ ] 회차별로 자동차를 전진시킨 후 모든 회차가 끝난 후 우승한 자동차를 선정한다 + - [ ] 회차별로 각 자동차 별로 0~9사이의 무작위 값을 생성한다. + - [ ] 무작위 값 생성 시 테스트를 위해 난수 생성기를 주입할 수 있도록 설계한다. + - [ ] 자동차 이동 여부 판정: + - [ ] 생성된 값이 4 이상이면 1칸 이동 + - [ ] 생성된 값이 4 미만이면 변화 없음 + - [ ] 회차별로 이동한 횟수만큼 "-"를 통해 이동 상태를 표시한다. + - [ ] 모든 회차가 끝난 후 가장 많이 이동한 자동차를 우승자로 선정한다. + - [ ] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분 + +**예외처리** + +`IllegalArgumentException` 이후 프로그램이 종료되도록 한다. + +- [ ] **자동차 이름 입력 예외처리** + - [ ] 자동차 이름 길이가 5자를 넘는 경우 + - [ ] 빈 이름이 포함된 경우의 자동차 이름 + - [ ] 같은 이름이 있는 경우 중복 처리 +- [ ] **시도할 횟수 예외처리** + - [ ] 숫자가 아닌 경우에 대한 처리 + - [ ] 음수 또는 0일 경우에 대한 처리 + - [ ] 빈 입력에 대한 처리 \ No newline at end of file From 3e4be2e28e53eb93941472d5197acbc9381a31cb Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 13:44:29 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99=20=EC=A0=84=EB=9E=B5=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../racingcar/domain/strategy/MoveStrategy.java | 6 ++++++ .../domain/strategy/RandomMoveStrategy.java | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/racingcar/domain/strategy/MoveStrategy.java create mode 100644 src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java diff --git a/src/main/java/racingcar/domain/strategy/MoveStrategy.java b/src/main/java/racingcar/domain/strategy/MoveStrategy.java new file mode 100644 index 0000000..b6b30bb --- /dev/null +++ b/src/main/java/racingcar/domain/strategy/MoveStrategy.java @@ -0,0 +1,6 @@ +package racingcar.domain.strategy; + +@FunctionalInterface +public interface MoveStrategy { + boolean movable(); +} diff --git a/src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java b/src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java new file mode 100644 index 0000000..518bfd8 --- /dev/null +++ b/src/main/java/racingcar/domain/strategy/RandomMoveStrategy.java @@ -0,0 +1,16 @@ +package racingcar.domain.strategy; + +import camp.nextstep.edu.missionutils.Randoms; + +public class RandomMoveStrategy implements MoveStrategy { + + private static final int RANDOM_MIN = 0; + private static final int RANDOM_MAX = 9; + private static final int MIN_MOVE_NUMBER = 4; + + @Override + public boolean movable() { + int number = Randoms.pickNumberInRange(RANDOM_MIN, RANDOM_MAX); + return number >= MIN_MOVE_NUMBER; + } +} From c965d4075cd9157faa47b3e820ee8536a20da349 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 13:45:17 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EC=9D=98=20=EC=9D=B4=EB=A6=84=EA=B3=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EB=A5=BC=20=EA=B0=80=EC=A7=80=EB=8A=94=20?= =?UTF-8?q?=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/domain/Car.java | 54 +++++++++++++++++++ .../java/racingcar/util/ErrorMessages.java | 9 ++++ 2 files changed, 63 insertions(+) create mode 100644 src/main/java/racingcar/domain/Car.java create mode 100644 src/main/java/racingcar/util/ErrorMessages.java diff --git a/src/main/java/racingcar/domain/Car.java b/src/main/java/racingcar/domain/Car.java new file mode 100644 index 0000000..f7cfd1b --- /dev/null +++ b/src/main/java/racingcar/domain/Car.java @@ -0,0 +1,54 @@ +package racingcar.domain; + +import racingcar.domain.strategy.MoveStrategy; +import racingcar.util.ErrorMessages; + +import java.util.Objects; + +public class Car { + private static final int START_POSITION = 0; + private static final int MAX_CAR_NAME_LENGTH = 5; + + private final String carName; + private int position = START_POSITION; + + public Car(String carName) { + validateCarName(carName); + this.carName = carName; + } + + private void validateCarName(String carName) { + if (carName == null || carName.isBlank()) { + throw new IllegalArgumentException(ErrorMessages.INVALID_CAR_NAME_BLANK); + } + if (carName.length() > MAX_CAR_NAME_LENGTH) { + throw new IllegalArgumentException(ErrorMessages.TOO_LONG_CAR_NAME); + } + } + public void move(MoveStrategy strategy) { + if (strategy.movable()) { + position++; + } + } + + public String getName() { + return carName; + } + + public int getPosition() { + return position; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Car)) return false; + Car car = (Car) o; + return Objects.equals(carName, car.carName); + } + + @Override + public int hashCode() { + return Objects.hash(carName); + } +} diff --git a/src/main/java/racingcar/util/ErrorMessages.java b/src/main/java/racingcar/util/ErrorMessages.java new file mode 100644 index 0000000..d7c562f --- /dev/null +++ b/src/main/java/racingcar/util/ErrorMessages.java @@ -0,0 +1,9 @@ +package racingcar.util; + +public class ErrorMessages { + private static final String ERROR_PREFIX = "[ERROR] "; + + public static final String INVALID_CAR_NAME_BLANK = ERROR_PREFIX + "자동차 이름은 비어 있을 수 없습니다."; + public static final String TOO_LONG_CAR_NAME = ERROR_PREFIX + "자동차 이름은 5자 이하여야 합니다."; + +} From d53fc0fbe5abbf24e36846a637c5087d230ed656 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 14:58:59 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=EC=97=AC=EB=9F=AC=EA=B0=9C=EB=A5=BC=20=EC=A7=91=ED=95=A9?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EA=B4=80=EB=A6=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=89=EC=BB=AC=EB=A0=89=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/domain/Cars.java | 52 +++++++++++++++++++ .../java/racingcar/util/ErrorMessages.java | 1 + 2 files changed, 53 insertions(+) create mode 100644 src/main/java/racingcar/domain/Cars.java diff --git a/src/main/java/racingcar/domain/Cars.java b/src/main/java/racingcar/domain/Cars.java new file mode 100644 index 0000000..99e5949 --- /dev/null +++ b/src/main/java/racingcar/domain/Cars.java @@ -0,0 +1,52 @@ +package racingcar.domain; + +import racingcar.domain.strategy.MoveStrategy; +import racingcar.util.ErrorMessages; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class Cars { + private static final int DEFAULT_POSITION = 0; + + private final List cars; + + public Cars(List carNames) { + List carList = carNames.stream() + .map(Car::new) + .toList(); + validateDuplicateCars(carList); + this.cars = carList; + } + + private void validateDuplicateCars(List carList) { + Set uniqueCars = new HashSet<>(carList); + if (uniqueCars.size() != carList.size()) { + throw new IllegalArgumentException(ErrorMessages.DUPLICATE_CAR_NAME); + } + } + + public void moveAll(MoveStrategy strategy) { + cars.forEach(car -> car.move(strategy)); + } + + public List getCars() { + return Collections.unmodifiableList(cars); + } + + public List findWinners() { + int maxPosition = findMaxPosition(); + return cars.stream() + .filter(car -> car.getPosition() == maxPosition) + .toList(); + } + + private int findMaxPosition() { + return cars.stream() + .mapToInt(Car::getPosition) + .max() + .orElse(DEFAULT_POSITION); + } +} diff --git a/src/main/java/racingcar/util/ErrorMessages.java b/src/main/java/racingcar/util/ErrorMessages.java index d7c562f..5d1133a 100644 --- a/src/main/java/racingcar/util/ErrorMessages.java +++ b/src/main/java/racingcar/util/ErrorMessages.java @@ -5,5 +5,6 @@ public class ErrorMessages { public static final String INVALID_CAR_NAME_BLANK = ERROR_PREFIX + "자동차 이름은 비어 있을 수 없습니다."; public static final String TOO_LONG_CAR_NAME = ERROR_PREFIX + "자동차 이름은 5자 이하여야 합니다."; + public static final String DUPLICATE_CAR_NAME = ERROR_PREFIX + "중복된 자동차 이름이 존재합니다."; } From 4cdea5b45db2e4c548c45bb6b7d4d76c4d04823c Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 15:28:07 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat:=20=EC=9A=94=EA=B5=AC=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=ED=9D=90=EB=A6=84=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=A1=B0=EB=A6=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/Application.java | 8 ++- src/main/java/racingcar/config/AppConfig.java | 26 ++++++++ .../controller/RacingGameController.java | 59 +++++++++++++++++++ .../racingcar/service/RacingGameService.java | 12 ++++ .../java/racingcar/util/ErrorMessages.java | 1 + src/main/java/racingcar/view/InputView.java | 18 ++++++ src/main/java/racingcar/view/OutputView.java | 26 ++++++++ 7 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/main/java/racingcar/config/AppConfig.java create mode 100644 src/main/java/racingcar/controller/RacingGameController.java create mode 100644 src/main/java/racingcar/service/RacingGameService.java create mode 100644 src/main/java/racingcar/view/InputView.java create mode 100644 src/main/java/racingcar/view/OutputView.java diff --git a/src/main/java/racingcar/Application.java b/src/main/java/racingcar/Application.java index a17a52e..997a895 100644 --- a/src/main/java/racingcar/Application.java +++ b/src/main/java/racingcar/Application.java @@ -1,7 +1,13 @@ package racingcar; +import racingcar.config.AppConfig; +import racingcar.controller.RacingGameController; +import racingcar.domain.strategy.RandomMoveStrategy; + public class Application { public static void main(String[] args) { - // TODO: 프로그램 구현 + AppConfig config = new AppConfig(); + RacingGameController controller = config.racingGameController(); + controller.run(new RandomMoveStrategy()); } } diff --git a/src/main/java/racingcar/config/AppConfig.java b/src/main/java/racingcar/config/AppConfig.java new file mode 100644 index 0000000..830f539 --- /dev/null +++ b/src/main/java/racingcar/config/AppConfig.java @@ -0,0 +1,26 @@ +package racingcar.config; + + +import racingcar.controller.RacingGameController; +import racingcar.service.RacingGameService; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +public class AppConfig { + + public RacingGameController racingGameController() { + return new RacingGameController(inputView(), outputView(), racingGameService()); + } + + public InputView inputView() { + return new InputView(); + } + + public OutputView outputView() { + return new OutputView(); + } + + public RacingGameService racingGameService() { + return new RacingGameService(); + } +} \ No newline at end of file diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java new file mode 100644 index 0000000..5e03b9d --- /dev/null +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -0,0 +1,59 @@ +package racingcar.controller; + +import racingcar.domain.Cars; +import racingcar.domain.strategy.MoveStrategy; +import racingcar.service.RacingGameService; +import racingcar.util.ErrorMessages; +import racingcar.view.InputView; +import racingcar.view.OutputView; + +import java.util.Arrays; +import java.util.List; + +public class RacingGameController { + private final InputView inputView; + private final OutputView outputView; + private final RacingGameService racingGameService; + + public RacingGameController(InputView inputView, OutputView outputView, RacingGameService racingGameService) { + this.inputView = inputView; + this.outputView = outputView; + this.racingGameService = racingGameService; + } + public void run(MoveStrategy strategy) { + Cars cars = createRacingCars(); + int tryCount = parsingTryCounts(inputView.readTryCount()); + repeatMoveAndPrint(cars, tryCount, strategy); + outputView.printWinners(cars.findWinners()); + + } + + + private void repeatMoveAndPrint(Cars cars, int tryCount, MoveStrategy strategy) { + for (int i = 0; i < tryCount; i++) { + cars.moveAll(strategy); + outputView.printCars(cars.getCars()); + } + } + + private Cars createRacingCars() { + List carNames = parsingCarNames(inputView.createCarNames()); + return racingGameService.createCars(carNames); + + } + + private int parsingTryCounts(String tryCount) { + try { + return Integer.parseInt(tryCount); + }catch (NumberFormatException e){ + throw new IllegalArgumentException(ErrorMessages.INVALID_TRY_COUNT); + } + } + + private List parsingCarNames(String carNames) { + return Arrays.stream(carNames.split(",")) + .map(String::trim) + .filter(name -> !name.isBlank()) + .toList(); + } +} diff --git a/src/main/java/racingcar/service/RacingGameService.java b/src/main/java/racingcar/service/RacingGameService.java new file mode 100644 index 0000000..58757e1 --- /dev/null +++ b/src/main/java/racingcar/service/RacingGameService.java @@ -0,0 +1,12 @@ +package racingcar.service; + +import racingcar.domain.Cars; + +import java.util.List; + +public class RacingGameService { + + public Cars createCars(List names) { + return new Cars(names); + } +} diff --git a/src/main/java/racingcar/util/ErrorMessages.java b/src/main/java/racingcar/util/ErrorMessages.java index 5d1133a..c176f70 100644 --- a/src/main/java/racingcar/util/ErrorMessages.java +++ b/src/main/java/racingcar/util/ErrorMessages.java @@ -6,5 +6,6 @@ public class ErrorMessages { public static final String INVALID_CAR_NAME_BLANK = ERROR_PREFIX + "자동차 이름은 비어 있을 수 없습니다."; public static final String TOO_LONG_CAR_NAME = ERROR_PREFIX + "자동차 이름은 5자 이하여야 합니다."; public static final String DUPLICATE_CAR_NAME = ERROR_PREFIX + "중복된 자동차 이름이 존재합니다."; + public static final String INVALID_TRY_COUNT = ERROR_PREFIX + "시도횟수는 숫자만 입력 가능합니다"; } diff --git a/src/main/java/racingcar/view/InputView.java b/src/main/java/racingcar/view/InputView.java new file mode 100644 index 0000000..1d6b738 --- /dev/null +++ b/src/main/java/racingcar/view/InputView.java @@ -0,0 +1,18 @@ +package racingcar.view; + +import camp.nextstep.edu.missionutils.Console; + +public class InputView { + private static final String INPUT_CAR_NAMES_MESSAGE = "경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기준으로 구분)"; + private static final String INPUT_TRY_COUNT_MESSAGE = "시도할 회수는 몇회인가요?"; + + public String createCarNames() { + System.out.println(INPUT_CAR_NAMES_MESSAGE); + return Console.readLine().trim(); + } + + public String readTryCount() { + System.out.println(INPUT_TRY_COUNT_MESSAGE); + return Console.readLine().trim(); + } +} diff --git a/src/main/java/racingcar/view/OutputView.java b/src/main/java/racingcar/view/OutputView.java new file mode 100644 index 0000000..a0c7e91 --- /dev/null +++ b/src/main/java/racingcar/view/OutputView.java @@ -0,0 +1,26 @@ +package racingcar.view; + +import racingcar.domain.Car; +import java.util.List; +import java.util.stream.Collectors; + +public class OutputView { + + private static final String WINNER_ANNOUNCEMENT_PREFIX = "최종 우승자 : "; + private static final String NAME_POSITION_SEPARATOR = " : "; + private static final String POSITION_SYMBOL = "-"; + + public void printCars(List cars) { + for (Car car : cars) { + System.out.println(car.getName() + NAME_POSITION_SEPARATOR + POSITION_SYMBOL.repeat(car.getPosition())); + } + System.out.println(); + } + + public void printWinners(List winners) { + String winnerNames = winners.stream() + .map(Car::getName) + .collect(Collectors.joining(",")); + System.out.println(WINNER_ANNOUNCEMENT_PREFIX + winnerNames); + } +} \ No newline at end of file From 6a55c1787a45805517e47b3c2b144ba8e2cd27cd Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 15:41:32 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20=EC=8B=9C=EB=8F=84=ED=9A=9F?= =?UTF-8?q?=EC=88=98=EB=A5=BC=20=EA=B0=9D=EC=B2=B4=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RacingGameController.java | 5 ++-- src/main/java/racingcar/domain/TryCount.java | 23 +++++++++++++++++++ .../java/racingcar/util/ErrorMessages.java | 3 ++- 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 src/main/java/racingcar/domain/TryCount.java diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java index 5e03b9d..4ae80f0 100644 --- a/src/main/java/racingcar/controller/RacingGameController.java +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -1,6 +1,7 @@ package racingcar.controller; import racingcar.domain.Cars; +import racingcar.domain.TryCount; import racingcar.domain.strategy.MoveStrategy; import racingcar.service.RacingGameService; import racingcar.util.ErrorMessages; @@ -22,8 +23,8 @@ public RacingGameController(InputView inputView, OutputView outputView, RacingGa } public void run(MoveStrategy strategy) { Cars cars = createRacingCars(); - int tryCount = parsingTryCounts(inputView.readTryCount()); - repeatMoveAndPrint(cars, tryCount, strategy); + TryCount tryCount = new TryCount(parsingTryCounts(inputView.readTryCount())); + repeatMoveAndPrint(cars, tryCount.getValue(), strategy); outputView.printWinners(cars.findWinners()); } diff --git a/src/main/java/racingcar/domain/TryCount.java b/src/main/java/racingcar/domain/TryCount.java new file mode 100644 index 0000000..af489b7 --- /dev/null +++ b/src/main/java/racingcar/domain/TryCount.java @@ -0,0 +1,23 @@ +package racingcar.domain; + +import racingcar.util.ErrorMessages; + +public class TryCount { + private static final int MIN_TRY_COUNT = 1; + private final int value; + + public TryCount(Integer value) { + validate(value); + this.value = value; + } + + private void validate(int value) { + if (value < MIN_TRY_COUNT) { + throw new IllegalArgumentException(ErrorMessages.INVALID_TRY_COUNT_OVER_ONE); + } + } + + public int getValue() { + return value; + } +} diff --git a/src/main/java/racingcar/util/ErrorMessages.java b/src/main/java/racingcar/util/ErrorMessages.java index c176f70..1410c48 100644 --- a/src/main/java/racingcar/util/ErrorMessages.java +++ b/src/main/java/racingcar/util/ErrorMessages.java @@ -6,6 +6,7 @@ public class ErrorMessages { public static final String INVALID_CAR_NAME_BLANK = ERROR_PREFIX + "자동차 이름은 비어 있을 수 없습니다."; public static final String TOO_LONG_CAR_NAME = ERROR_PREFIX + "자동차 이름은 5자 이하여야 합니다."; public static final String DUPLICATE_CAR_NAME = ERROR_PREFIX + "중복된 자동차 이름이 존재합니다."; - public static final String INVALID_TRY_COUNT = ERROR_PREFIX + "시도횟수는 숫자만 입력 가능합니다"; + public static final String INVALID_TRY_COUNT = ERROR_PREFIX + "시도 횟수는 숫자만 입력 가능합니다"; + public static final String INVALID_TRY_COUNT_OVER_ONE = ERROR_PREFIX + "시도 횟수는 1이상만 입력 가능합니다"; } From 034c66ab296c686559c1c0c0b076dc6562a309c6 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 15:48:05 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EC=9E=85=EB=A0=A5=ED=8C=8C?= =?UTF-8?q?=EC=8B=B1=EC=9D=84=20=ED=81=B4=EB=9E=98=EC=8A=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RacingGameController.java | 23 +++---------------- src/main/java/racingcar/util/InputParser.java | 22 ++++++++++++++++++ 2 files changed, 25 insertions(+), 20 deletions(-) create mode 100644 src/main/java/racingcar/util/InputParser.java diff --git a/src/main/java/racingcar/controller/RacingGameController.java b/src/main/java/racingcar/controller/RacingGameController.java index 4ae80f0..b9740d5 100644 --- a/src/main/java/racingcar/controller/RacingGameController.java +++ b/src/main/java/racingcar/controller/RacingGameController.java @@ -4,9 +4,9 @@ import racingcar.domain.TryCount; import racingcar.domain.strategy.MoveStrategy; import racingcar.service.RacingGameService; -import racingcar.util.ErrorMessages; import racingcar.view.InputView; import racingcar.view.OutputView; +import racingcar.util.InputParser; import java.util.Arrays; import java.util.List; @@ -23,13 +23,12 @@ public RacingGameController(InputView inputView, OutputView outputView, RacingGa } public void run(MoveStrategy strategy) { Cars cars = createRacingCars(); - TryCount tryCount = new TryCount(parsingTryCounts(inputView.readTryCount())); + TryCount tryCount = new TryCount(InputParser.parseTryCount(inputView.readTryCount())); repeatMoveAndPrint(cars, tryCount.getValue(), strategy); outputView.printWinners(cars.findWinners()); } - private void repeatMoveAndPrint(Cars cars, int tryCount, MoveStrategy strategy) { for (int i = 0; i < tryCount; i++) { cars.moveAll(strategy); @@ -38,23 +37,7 @@ private void repeatMoveAndPrint(Cars cars, int tryCount, MoveStrategy strategy) } private Cars createRacingCars() { - List carNames = parsingCarNames(inputView.createCarNames()); + List carNames = InputParser.parseCarNames(inputView.createCarNames()); return racingGameService.createCars(carNames); - - } - - private int parsingTryCounts(String tryCount) { - try { - return Integer.parseInt(tryCount); - }catch (NumberFormatException e){ - throw new IllegalArgumentException(ErrorMessages.INVALID_TRY_COUNT); - } - } - - private List parsingCarNames(String carNames) { - return Arrays.stream(carNames.split(",")) - .map(String::trim) - .filter(name -> !name.isBlank()) - .toList(); } } diff --git a/src/main/java/racingcar/util/InputParser.java b/src/main/java/racingcar/util/InputParser.java new file mode 100644 index 0000000..ea26ff0 --- /dev/null +++ b/src/main/java/racingcar/util/InputParser.java @@ -0,0 +1,22 @@ +package racingcar.util; + +import java.util.Arrays; +import java.util.List; + +public class InputParser { + + public static List parseCarNames(String input) { + return Arrays.stream(input.split(",")) + .map(String::trim) + .filter(name -> !name.isBlank()) + .toList(); + } + + public static int parseTryCount(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(ErrorMessages.INVALID_TRY_COUNT); + } + } +} From f2b8b16a126e5e3a60883b58904464edf7023340 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 15:49:19 +0900 Subject: [PATCH 08/10] =?UTF-8?q?docs:=20=EA=B8=B0=EB=8A=A5=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b073247..e177167 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,26 @@ ### 기능구현 목록 -- [ ] 경주할 자동차들의 이름과 시도할 횟수를 입력받아 저장한다. - - [ ] 유효성 검사와 예외 처리를 통해 올바른 입력만 허용한다. (예: 빈 입력, 중복된 이름, 자동차 이름 길이 등) -- [ ] 회차별로 자동차를 전진시킨 후 모든 회차가 끝난 후 우승한 자동차를 선정한다 - - [ ] 회차별로 각 자동차 별로 0~9사이의 무작위 값을 생성한다. - - [ ] 무작위 값 생성 시 테스트를 위해 난수 생성기를 주입할 수 있도록 설계한다. - - [ ] 자동차 이동 여부 판정: - - [ ] 생성된 값이 4 이상이면 1칸 이동 - - [ ] 생성된 값이 4 미만이면 변화 없음 - - [ ] 회차별로 이동한 횟수만큼 "-"를 통해 이동 상태를 표시한다. - - [ ] 모든 회차가 끝난 후 가장 많이 이동한 자동차를 우승자로 선정한다. - - [ ] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분 +- [x] 경주할 자동차들의 이름과 시도할 횟수를 입력받는다 + - [x] 유효성 검사와 예외 처리를 통해 올바른 입력만 허용한다. (예: 빈 입력, 중복된 이름, 자동차 이름 길이 등) +- [x] 회차별로 자동차를 전진시킨 후 모든 회차가 끝난 후 우승한 자동차를 선정한다 + - [x] 회차별로 각 자동차 별로 0~9사이의 무작위 값을 생성한다. + - [x] 무작위 값 생성 시 테스트를 위해 난수 생성기를 주입할 수 있도록 설계한다. + - [x] 자동차 이동 여부 판정: + - [x] 생성된 값이 4 이상이면 1칸 이동 + - [x] 생성된 값이 4 미만이면 변화 없음 + - [x] 회차별로 이동한 횟수만큼 "-"를 통해 이동 상태를 표시한다. + - [x] 모든 회차가 끝난 후 가장 많이 이동한 자동차를 우승자로 선정한다. + - [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분 **예외처리** `IllegalArgumentException` 이후 프로그램이 종료되도록 한다. -- [ ] **자동차 이름 입력 예외처리** - - [ ] 자동차 이름 길이가 5자를 넘는 경우 - - [ ] 빈 이름이 포함된 경우의 자동차 이름 - - [ ] 같은 이름이 있는 경우 중복 처리 -- [ ] **시도할 횟수 예외처리** - - [ ] 숫자가 아닌 경우에 대한 처리 - - [ ] 음수 또는 0일 경우에 대한 처리 - - [ ] 빈 입력에 대한 처리 \ No newline at end of file +- [x] **자동차 이름 입력 예외처리** + - [x] 자동차 이름 길이가 5자를 넘는 경우 + - [x] 빈 이름이 포함된 경우의 자동차 이름 + - [x] 같은 이름이 있는 경우 중복 처리 +- [x] **시도할 횟수 예외처리** + - [x] 숫자가 아닌 경우에 대한 처리 + - [x] 음수 또는 0일 경우에 대한 처리 + - [x] 빈 입력에 대한 처리 \ No newline at end of file From 98af84ddd3b9c65b2d88ede3e651edca2cb6bc92 Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Mon, 7 Apr 2025 16:12:04 +0900 Subject: [PATCH 09/10] =?UTF-8?q?docs:=20=EC=9D=B4=ED=8E=99=ED=8B=B0?= =?UTF-8?q?=EB=B8=8C=20=EC=B1=85=20=EC=8A=A4=ED=84=B0=EB=94=94=20=EB=82=B4?= =?UTF-8?q?=EC=9A=A9=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e177167..be919e5 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,100 @@ - [x] **시도할 횟수 예외처리** - [x] 숫자가 아닌 경우에 대한 처리 - [x] 음수 또는 0일 경우에 대한 처리 - - [x] 빈 입력에 대한 처리 \ No newline at end of file + - [x] 빈 입력에 대한 처리 + +--- +# 이펙티브 스터디 + +## 아이템 17. 변경 가능성을 최소화하라 + +불변 클래스란? + +- 그 인스턴스의 내부 값을 수정할 수 없는 클래스. + 불변 인스턴스에 간직된 정보는 고정되어 객체가 파괴되 전까지 절대객체릐 달라지지 않음 + +### **클래스를 불변으로 만드려면** + +- 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다 +- 클래스를 확장할 수 없도록 한다 +- 모든 필드를 final로 선언한다 +- 모든 필드를 private로 선언 한다 +- 자신 외에는 내부의 가변 컴포넌트에 접근 할 수 없도록 한다 + +정적 팩터리를 사용한 불변 클래스 + +```java +public final class Money { + private final int amount; + private final String currency; + + // 생성자는 private → 외부에서 new로 만들 수 없음 + private Money(int amount, String currency) { + this.amount = amount; + this.currency = currency; + } + + // 정적 팩토리 메서드 (이름 있는 생성자 역할) + public static Money of(int amount, String currency) { + return new Money(amount, currency); + } +} +``` + +### 코드에서의 활용 + +**TryCount를 불변 클래스로 사용** + +- 객체에서 변경되어서는 안되는 필드를 private, final 사용함 + +Cars는 일급 컬렉션으로 컬렉션 자체는 불변이지만 내부 요소는 가변 + +- Car의 경우는 움직여야 하기 때문 + +생성자가 하나뿐이기에 정적 팩터리메서드 사용 X + +## 아이템18. 상속보다는 컴포지션을 사용하라 + +### 컴포지션 이란? + +어떤 객체가 다른 객체를 포함해서 사용하는것(has-a 관계) + +어떤 객체가 다른 객체의 기능 을 사용하고 싶을때 그 객체를 자신의 필드로 가지고 있다가 필요한 시점에 위임 하는 방식 + +상속**(inheritance)은 강력하지만, 잘못 사용하면 결합도(coupling)가 높아지고, 유연성 떨어지며, 오류를 유발가능** + +부모클래스의 내부 구현에 강하게 의존하므로, 부모클래스가 바뀌면 자식클래스도 영향을 크게 받음 + +**컴포지션(composition)은 필드로 다른 객체를 가지고 있으며, 그 객체에 기능을 위임함으로 더 유연하고 안정적 설계가 가능** + +### 코드에서의 활용 + +- 자동차 전진 조건을 전략 패턴을 이용함 +- **Car가 직접 이동 조건을 판단하지 않고, 전략 객체에게 위임** +- 필드로 가지고 있지않지만 이걸 컴포지션이라고 볼 수 있는지가 궁금 + +## 아이템 10, 11 equals 와 hashcode + +**equals(Object obj)** + +- 두 객체가 논리적으로 같은지 비교 +- Object 기본 구현은 단순히 주소비교 (==) +- 필요시 오버라이딩 해서 본인이 정의한 동등성 기준 넣어야함 + +**hashCode()** + +- 객체를 식별하는 **정수 해시값** 반환 +- HashSet, HashMap 같은 **해시 기반 자료구조**에서 필수 +- equals()가 true라면 **hashCode도 반드시 같아야 함** + +### **equals 재정의 → 반드시 hashCode도 재정의** + +- 객체가 논리적으로 같은지 비교시 먼저 **hashCode 비교후 equals 호출** + +### 코드에서의 활용 + +- 자동차 이름이 같으면 안된다. 이것을 객체 단에서 처리 + +equals(Object obj) → 객체가 같다는 기준 정의 (역기서는 자동차의 이름) + +hashCode() → `Set uniqueCars = new HashSet<>(carList);` 으로 중복검사 위해서 \ No newline at end of file From 3e6f7192bb9b08c63ad004394588b079786f00ee Mon Sep 17 00:00:00 2001 From: JinHyeon Date: Wed, 9 Apr 2025 19:10:33 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20=EC=A0=91=EA=B7=BC=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EC=9E=90=20private=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/racingcar/config/AppConfig.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/racingcar/config/AppConfig.java b/src/main/java/racingcar/config/AppConfig.java index 830f539..b5dfe9b 100644 --- a/src/main/java/racingcar/config/AppConfig.java +++ b/src/main/java/racingcar/config/AppConfig.java @@ -12,15 +12,15 @@ public RacingGameController racingGameController() { return new RacingGameController(inputView(), outputView(), racingGameService()); } - public InputView inputView() { + private InputView inputView() { return new InputView(); } - public OutputView outputView() { + private OutputView outputView() { return new OutputView(); } - public RacingGameService racingGameService() { + private RacingGameService racingGameService() { return new RacingGameService(); } } \ No newline at end of file