From 8825da2c008291da32f4c44b28a76472228340cf Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 13:20:23 +0900 Subject: [PATCH 1/9] =?UTF-8?q?docs=20:=204=EB=8B=A8=EA=B3=84=20TODO=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ba24573e89a..90a3dc54999 100644 --- a/README.md +++ b/README.md @@ -37,5 +37,83 @@ ### 개발 구조 Game : 게임의 시작과 종료를 담당 Race : 전체 라운드의 진행을 담당 +GameSettings : 자동차 경주 게임의 설정값 +Car : 자동차 객체 InputView : 사용자의 입력을 받음 -ResultView : 결과를 출력 \ No newline at end of file +ResultView : 결과를 출력 + +## 🚀 4단계 - 자동차 경주(우승자) + +### 기능 요구사항 + +- 각 자동차에 이름을 부여할 수 있다. 자동차 이름은 5자를 초과할 수 없다. +- 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. +- 자동차 이름은 쉼표(,)를 기준으로 구분한다. +- 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다. + +### 구현 TODO 목록 + +1. **자동차 이름 기능** + + - [ ] `Car` 클래스에 이름 필드 추가 + - [ ] 이름 유효성 검사 (5자 초과 불가) + - [ ] `Car` 생성자에 이름 파라미터 추가 + - [ ] `CarTest`에 이름 관련 테스트 추가 + - [ ] 이름이 5자를 초과하면 예외 발생 + - [ ] 유효한 이름으로 생성 가능 + +2. **자동차 이름 출력** + + - [ ] `ResultView` 수정 + - [ ] 자동차 이름과 위치를 함께 출력하는 형식 변경 + - [ ] `presentCars` 메소드 파라미터 수정 (이름 정보 추가) + - [ ] `Race` 클래스 수정 + - [ ] `getCarPositions` 메소드를 `getCarStatus`로 변경 + - [ ] 자동차 이름과 위치를 함께 반환하도록 수정 + +3. **자동차 이름 입력** + + - [ ] `InputView` 수정 + - [ ] 자동차 이름 입력 메소드 추가 + - [ ] 쉼표로 구분된 이름 문자열 파싱 로직 추가 + - [ ] `GameSettings` 수정 + - [ ] 자동차 이름 목록 필드 추가 + - [ ] 생성자 수정 + +4. **우승자 판정** + + - [ ] `Race` 클래스에 우승자 판정 로직 추가 + - [ ] `getWinners` 메소드 추가 + - [ ] 가장 멀리 간 자동차들 찾기 + - [ ] `ResultView`에 우승자 출력 메소드 추가 + - [ ] `Game` 클래스 수정 + - [ ] 경주 종료 후 우승자 출력 로직 추가 + +5. **테스트 추가** + + - [ ] `RaceTest`에 우승자 판정 테스트 추가 + - [ ] 단일 우승자 케이스 + - [ ] 다중 우승자 케이스 + - [ ] `InputViewTest` 추가 + - [ ] 자동차 이름 입력 테스트 + - [ ] 잘못된 입력 처리 테스트 + +6. **리팩토링 고려사항** + - [ ] `Car` 클래스에 `Position` 값 객체 도입 검토 + - [ ] `CarName` 값 객체 도입 검토 + - [ ] `CarStatus` DTO 도입 검토 (이름과 위치를 함께 전달하기 위해) + +### 프로그래밍 요구사항 +* indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + * 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + * 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. +* 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. + * 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. +* 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 + * 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + * UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +* 자바 코드 컨벤션을 지키면서 프로그래밍한다. + * 참고문서: https://google.github.io/styleguide/javaguide.html 또는 https://myeonguni.tistory.com/1596 +* else 예약어를 쓰지 않는다. + * 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + * else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. From c4325fc609f502f0202aec0c413926a1864ebb0f Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 14:04:41 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat=20:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EA=B8=B0=EB=8A=A5=20=20=20=20-=20[x]=20`C?= =?UTF-8?q?ar`=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=97=90=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20=20=20-=20[x]?= =?UTF-8?q?=20=EC=9D=B4=EB=A6=84=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=20(5=EC=9E=90=20=EC=B4=88=EA=B3=BC=20=EB=B6=88?= =?UTF-8?q?=EA=B0=80)=20=20=20=20-=20[x]=20`Car`=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=EC=9E=90=EC=97=90=20=EC=9D=B4=EB=A6=84=20=ED=8C=8C=EB=9D=BC?= =?UTF-8?q?=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80=20=20=20=20-=20[x]=20`C?= =?UTF-8?q?arTest`=EC=97=90=20=EC=9D=B4=EB=A6=84=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20=20=20=20=20?= =?UTF-8?q?=20-=20[x]=20=EC=9D=B4=EB=A6=84=EC=9D=B4=205=EC=9E=90=EB=A5=BC?= =?UTF-8?q?=20=EC=B4=88=EA=B3=BC=ED=95=98=EB=A9=B4=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=20=20=20=20=20=20-=20[x]=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=ED=95=9C=20=EC=9D=B4=EB=A6=84=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++---- src/main/java/Car.java | 26 ++++++++++++++++ src/test/java/CarTest.java | 61 +++++++++++++++++++++++++++----------- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 90a3dc54999..b3a3913d5ac 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,12 @@ ResultView : 결과를 출력 1. **자동차 이름 기능** - - [ ] `Car` 클래스에 이름 필드 추가 - - [ ] 이름 유효성 검사 (5자 초과 불가) - - [ ] `Car` 생성자에 이름 파라미터 추가 - - [ ] `CarTest`에 이름 관련 테스트 추가 - - [ ] 이름이 5자를 초과하면 예외 발생 - - [ ] 유효한 이름으로 생성 가능 + - [x] `Car` 클래스에 이름 필드 추가 + - [x] 이름 유효성 검사 (5자 초과 불가) + - [x] `Car` 생성자에 이름 파라미터 추가 + - [x] `CarTest`에 이름 관련 테스트 추가 + - [x] 이름이 5자를 초과하면 예외 발생 + - [x] 유효한 이름으로 생성 가능 2. **자동차 이름 출력** diff --git a/src/main/java/Car.java b/src/main/java/Car.java index 5175e2535fe..0636bc5354b 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -1,8 +1,30 @@ public class Car { + @Deprecated + public static final String TEMP_CAR_NAME = "temp"; private static final int MOVEMENT_THRESHOLD = 4; + private final String name; private int position = 0; + @Deprecated + public Car() { + this.name = TEMP_CAR_NAME; + } + + public Car(String name) { + validateName(name); + this.name = name; + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Name cannot be blank"); + } + if (name.length() > 5) { + throw new IllegalArgumentException("Name cannot be longer than 5 characters"); + } + } + public void move(int seed) { if (seed < 0 || seed > 9) { throw new IllegalArgumentException("Invalid seed: " + seed); @@ -13,6 +35,10 @@ public void move(int seed) { } } + public String getName() { + return name; + } + public int getPosition() { return position; } diff --git a/src/test/java/CarTest.java b/src/test/java/CarTest.java index 17f6e41b84c..fd02795c76d 100644 --- a/src/test/java/CarTest.java +++ b/src/test/java/CarTest.java @@ -1,31 +1,56 @@ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; class CarTest { @Test - @DisplayName("랜덤 변수가 4이상이면 차가 한 칸 움직인다.") - void carMovesOneStepIfRandomNumberIsGreaterThanEqual4() { - for (int i = 0; i < 4; i++) { - Car car = new Car(); - car.move(i); - assertThat(car.getPosition()).isEqualTo(0); - } - for (int i = 4; i < 10; i++) { - Car car = new Car(); - car.move(i); - assertThat(car.getPosition()).isEqualTo(1); - } + @DisplayName("자동차는 이름을 가질 수 있다.") + void carHasName() { + Car car = new Car(); + assertThat(car.getName()).isEqualTo(Car.TEMP_CAR_NAME); + + car = new Car("MyCar"); + assertThat(car.getName()).isEqualTo("MyCar"); } @Test - @DisplayName("자동차를 움직이는 변수는 0에서 9사이의 값이다.") - void carMoveVariableIsBetween0And9() { - Car car = new Car(); - assertThrows(IllegalArgumentException.class, () -> car.move(-1)); - assertThrows(IllegalArgumentException.class, () -> car.move(10)); + @DisplayName("자동차 이름은 빈 문자열이 될 수 없다") + void carNameCannotBeEmpty() { + assertThatThrownBy(() -> new Car("")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be blank"); + } + + @Test + @DisplayName("자동차 이름은 공백만으로 구성될 수 없다") + void carNameCannotBeBlank() { + assertThatThrownBy(() -> new Car(" ")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be blank"); + } + + @Test + @DisplayName("자동차 이름은 5자를 초과할 수 없다") + void carNameCannotExceed5Characters() { + assertThatThrownBy(() -> new Car("123456")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be longer than 5 characters"); + } + + @ParameterizedTest + @DisplayName("random값이 4 이상일 경우에 자동차의 위치는 1 추가되고, 4 미만일 경우 위치가 변하지 않는다") + @CsvSource({"0, 0", "1, 0", "2, 0", "3, 0", "4, 1", "5, 1", "6, 1", "7, 1", "8, 1", "9, 1"}) + void carMovesAccordingToRandomValue(int seed, int expectedPosition) { + Car car = new Car("MyCar"); + car.move(seed); + assertThat(car.getPosition()).isEqualTo(expectedPosition); + } + + @ParameterizedTest + @DisplayName("이동을 위한 숫자는 0에서 9 사이여야 한다") + @ValueSource(ints = {-1, 10}) + void carMoveVariableIsBetween0And9(int invalidSeed) { + Car car = new Car("MyCar"); + assertThatThrownBy(() -> car.move(invalidSeed)).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid seed: " + invalidSeed); } } From be368144a68686fe5b80f53a352d60b00d40f515 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 14:31:26 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat=20:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=B6=9C=EB=A0=A5=20=20=20=20-=20[x]=20`R?= =?UTF-8?q?esultView`=20=EC=88=98=EC=A0=95=20=20=20=20=20=20-=20[x]=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84=EA=B3=BC=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5=ED=95=98=EB=8A=94=20=ED=98=95=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=20=20=20=20=20-=20[x]=20`presentCars`=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(=EC=9D=B4=EB=A6=84=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80)=20=20=20=20-=20[x]=20`Race`=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=88=98=EC=A0=95=20=20=20=20=20=20-=20[x?= =?UTF-8?q?]=20`getCarPositions`=20=EB=A9=94=EC=86=8C=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?`getCarStatus`=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=20=20=20=20=20-?= =?UTF-8?q?=20[x]=20=EC=9E=90=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84?= =?UTF-8?q?=EA=B3=BC=20=EC=9C=84=EC=B9=98=EB=A5=BC=20=ED=95=A8=EA=BB=98=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 95 +++++++++++++++++++---------------- src/main/java/CarStatus.java | 17 +++++++ src/main/java/Game.java | 3 +- src/main/java/Race.java | 8 +-- src/main/java/ResultView.java | 8 +-- src/test/java/RaceTest.java | 15 +++--- 6 files changed, 86 insertions(+), 60 deletions(-) create mode 100644 src/main/java/CarStatus.java diff --git a/README.md b/README.md index b3a3913d5ac..bf358def396 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,46 @@ # 자동차 경주 게임 + ## 진행 방법 -* 자동차 경주 게임 요구사항을 파악한다. -* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. -* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. -* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. + +- 자동차 경주 게임 요구사항을 파악한다. +- 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다. +- 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다. +- 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다. ## 온라인 코드 리뷰 과정 -* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) + +- [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview) ## 🚀 3단계 - 자동차 경주 ### 기능 요구사항 -* 초간단 자동차 경주 게임을 구현한다. -* 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. -* 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. -* 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다. -* 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. - -- [x] 자동차의 처음 위치는 0이다. -- [x] 사용자가 입력한 개수만큼의 자동차가 존재한다. -- [x] 사용자가 0 또는 음수의 자동차 개수를 입력하면 에러를 반환한다. -- [x] random값이 4 이상일 경우에 자동차의 위치는 1 추가된다. -- [x] m 라운드가 끝나면 자동차 경주는 종료된다. + +- 초간단 자동차 경주 게임을 구현한다. +- 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다. +- 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. +- 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다. +- 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다. + +* [x] 자동차의 처음 위치는 0이다. +* [x] 사용자가 입력한 개수만큼의 자동차가 존재한다. +* [x] 사용자가 0 또는 음수의 자동차 개수를 입력하면 에러를 반환한다. +* [x] random값이 4 이상일 경우에 자동차의 위치는 1 추가된다. +* [x] m 라운드가 끝나면 자동차 경주는 종료된다. ### 프로그래밍 요구사항 -* 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 - * 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. - * UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. -* 자바 코드 컨벤션을 지키면서 프로그래밍한다. - * 이 과정의 Code Style은 intellij idea Code Style. Java을 따른다. - * intellij idea Code Style. Java을 따르려면 code formatting 단축키(Windows : Ctrl + Alt + L. Mac : ⌥ (Option) + ⌘ (Command) + L.)를 사용한다. -* else 예약어를 쓰지 않는다. - * 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. - * else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + +- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 + - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + - UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. + - 이 과정의 Code Style은 intellij idea Code Style. Java을 따른다. + - intellij idea Code Style. Java을 따르려면 code formatting 단축키(Windows : Ctrl + Alt + L. Mac : ⌥ (Option) + ⌘ (Command) + L.)를 사용한다. +- else 예약어를 쓰지 않는다. + - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. ### 개발 구조 + Game : 게임의 시작과 종료를 담당 Race : 전체 라운드의 진행을 담당 GameSettings : 자동차 경주 게임의 설정값 @@ -64,12 +70,12 @@ ResultView : 결과를 출력 2. **자동차 이름 출력** - - [ ] `ResultView` 수정 - - [ ] 자동차 이름과 위치를 함께 출력하는 형식 변경 - - [ ] `presentCars` 메소드 파라미터 수정 (이름 정보 추가) - - [ ] `Race` 클래스 수정 - - [ ] `getCarPositions` 메소드를 `getCarStatus`로 변경 - - [ ] 자동차 이름과 위치를 함께 반환하도록 수정 + - [x] `ResultView` 수정 + - [x] 자동차 이름과 위치를 함께 출력하는 형식 변경 + - [x] `presentCars` 메소드 파라미터 수정 (이름 정보 추가) + - [x] `Race` 클래스 수정 + - [x] `getCarPositions` 메소드를 `getCarStatus`로 변경 + - [x] 자동차 이름과 위치를 함께 반환하도록 수정 3. **자동차 이름 입력** @@ -104,16 +110,17 @@ ResultView : 결과를 출력 - [ ] `CarStatus` DTO 도입 검토 (이름과 위치를 함께 전달하기 위해) ### 프로그래밍 요구사항 -* indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. - * 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. - * 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. -* 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. - * 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. -* 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 - * 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. - * UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. -* 자바 코드 컨벤션을 지키면서 프로그래밍한다. - * 참고문서: https://google.github.io/styleguide/javaguide.html 또는 https://myeonguni.tistory.com/1596 -* else 예약어를 쓰지 않는다. - * 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. - * else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. + +- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. + - 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. + - 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다. +- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. + - 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. +- 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외 + - 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다. + - UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다. +- 자바 코드 컨벤션을 지키면서 프로그래밍한다. + - 참고문서: https://google.github.io/styleguide/javaguide.html 또는 https://myeonguni.tistory.com/1596 +- else 예약어를 쓰지 않는다. + - 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다. + - else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. diff --git a/src/main/java/CarStatus.java b/src/main/java/CarStatus.java new file mode 100644 index 00000000000..1ba73da4c8f --- /dev/null +++ b/src/main/java/CarStatus.java @@ -0,0 +1,17 @@ +public class CarStatus { + private final String name; + private final int position; + + public CarStatus(Car car) { + this.name = car.getName(); + this.position = car.getPosition(); + } + + public String getName() { + return name; + } + + public int getPosition() { + return position; + } +} \ No newline at end of file diff --git a/src/main/java/Game.java b/src/main/java/Game.java index a12021dad10..642d47e3ea3 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -13,9 +13,10 @@ public void start() { Race race = new Race(settings); while (race.isRaceInProgress()) { + resultView.presentCars(race.getCarStatuses()); race.runRound(); - resultView.presentCars(race.getCarPositions()); } + resultView.presentCars(race.getCarStatuses()); } public static void main(String[] args) { diff --git a/src/main/java/Race.java b/src/main/java/Race.java index 652af54f1ff..ed061d05894 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -21,12 +21,12 @@ public Race(GameSettings settings) { } } - public List<Integer> getCarPositions() { - List<Integer> positions = new ArrayList<>(); + public List<CarStatus> getCarStatuses() { + List<CarStatus> statuses = new ArrayList<>(); for (Car car : cars) { - positions.add(car.getPosition()); + statuses.add(new CarStatus(car)); } - return positions; + return statuses; } public void runRound() { diff --git a/src/main/java/ResultView.java b/src/main/java/ResultView.java index 9c175aedb95..f1a27b1658e 100644 --- a/src/main/java/ResultView.java +++ b/src/main/java/ResultView.java @@ -6,9 +6,11 @@ public void presentStartMessage() { System.out.println("실행 결과"); } - public void presentCars(List<Integer> carPositions) { - for (int carPosition : carPositions) { - System.out.println("-".repeat(carPosition + 1)); + public void presentCars(List<CarStatus> cars) { + for (CarStatus car : cars) { + String positionIndicator = "-".repeat(car.getPosition() + 1); + String output = String.format("%s : %s", car.getName(), positionIndicator); + System.out.println(output); } System.out.println(); } diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index 7f7bdfd8251..d6b6e02c082 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -19,7 +19,7 @@ void negativeOrZeroCarCountInputReturnsError() { }); Race race = new Race(new GameSettings(1, 3)); - assertThat(race.getCarPositions()).hasSize(1); + assertThat(race.getCarStatuses()).hasSize(1); } @Test @@ -41,8 +41,8 @@ void negativeOrZeroRaceCountInputReturnsError() { @DisplayName("경기 시작시 자동차의 위치는 0이다.") void carPositionsAtStartAreZero() { Race race = new Race(new GameSettings(5, 3)); - for (Integer carPositions : race.getCarPositions()) { - assertThat(carPositions).isZero(); + for (CarStatus carStatus : race.getCarStatuses()) { + assertThat(carStatus.getPosition()).isZero(); } } @@ -50,14 +50,13 @@ void carPositionsAtStartAreZero() { @DisplayName("한 라운드가 진행되면 자동차의 위치는 기존 위치이거나, 기존 위치 + 1이다.") void carPositionsAfterOneRoundAreEitherSameOrIncremented() { Race race = new Race(new GameSettings(5, 3)); - race.runRound(); - List<Integer> initialPositions = race.getCarPositions(); + List<CarStatus> initialStatuses = race.getCarStatuses(); race.runRound(); - List<Integer> finalPositions = race.getCarPositions(); + List<CarStatus> finalStatuses = race.getCarStatuses(); - for (int i = 0; i < initialPositions.size(); i++) { - assertThat(finalPositions.get(i)).isIn(initialPositions.get(i), initialPositions.get(i) + 1); + for (int i = 0; i < initialStatuses.size(); i++) { + assertThat(finalStatuses.get(i).getPosition()).isIn(initialStatuses.get(i).getPosition(), initialStatuses.get(i).getPosition() + 1); } } From 040e2789426b816392bcc84cb1ddec67c6816a35 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 14:57:52 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat=20:=20=EC=9E=90=EB=8F=99=EC=B0=A8=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EC=9E=85=EB=A0=A5=20=20=20=20-=20[x]=20`I?= =?UTF-8?q?nputView`=20=EC=88=98=EC=A0=95=20=20=20=20=20=20-=20[x]=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20=20=20=20-=20[x]=20=EC=89=BC=ED=91=9C=EB=A1=9C=20?= =?UTF-8?q?=EA=B5=AC=EB=B6=84=EB=90=9C=20=EC=9D=B4=EB=A6=84=20=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=EC=97=B4=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=20=20=20-=20[x]=20`GameSettings`=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=20=20=20=20=20-=20[x]=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=20=EC=9D=B4=EB=A6=84=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20=20=20=20=20-=20[x?= =?UTF-8?q?]=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 +++---- src/main/java/Car.java | 11 ++----- src/main/java/GameSettings.java | 33 ++++++++++++++++--- src/main/java/InputView.java | 10 ++++-- src/main/java/Race.java | 8 ++--- src/test/java/CarTest.java | 5 +-- src/test/java/GameSettingsTest.java | 46 +++++++++++++++++++++++++++ src/test/java/RaceTest.java | 49 +++++++---------------------- 8 files changed, 105 insertions(+), 69 deletions(-) create mode 100644 src/test/java/GameSettingsTest.java diff --git a/README.md b/README.md index bf358def396..58a3e485f6b 100644 --- a/README.md +++ b/README.md @@ -79,12 +79,12 @@ ResultView : 결과를 출력 3. **자동차 이름 입력** - - [ ] `InputView` 수정 - - [ ] 자동차 이름 입력 메소드 추가 - - [ ] 쉼표로 구분된 이름 문자열 파싱 로직 추가 - - [ ] `GameSettings` 수정 - - [ ] 자동차 이름 목록 필드 추가 - - [ ] 생성자 수정 + - [x] `InputView` 수정 + - [x] 자동차 이름 입력 메소드 추가 + - [x] 쉼표로 구분된 이름 문자열 파싱 로직 추가 + - [x] `GameSettings` 수정 + - [x] 자동차 이름 목록 필드 추가 + - [x] 생성자 수정 4. **우승자 판정** diff --git a/src/main/java/Car.java b/src/main/java/Car.java index 0636bc5354b..1b454d7663e 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -1,23 +1,16 @@ public class Car { - @Deprecated - public static final String TEMP_CAR_NAME = "temp"; private static final int MOVEMENT_THRESHOLD = 4; private final String name; private int position = 0; - @Deprecated - public Car() { - this.name = TEMP_CAR_NAME; - } - public Car(String name) { validateName(name); - this.name = name; + this.name = name.trim(); } private void validateName(String name) { - if (name == null || name.isBlank()) { + if (name == null || name.trim().isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); } if (name.length() > 5) { diff --git a/src/main/java/GameSettings.java b/src/main/java/GameSettings.java index 6c9d813371a..13fc0509e14 100644 --- a/src/main/java/GameSettings.java +++ b/src/main/java/GameSettings.java @@ -1,14 +1,39 @@ public class GameSettings { - private final int carCount; + + private final String[] carNames; private final int roundCount; - public GameSettings(int carCount, int roundCount) { - this.carCount = carCount; + public GameSettings(String[] carNames, int roundCount) { + validateCarNames(carNames); + validateRoundCount(roundCount); + + this.carNames = carNames; this.roundCount = roundCount; } + private void validateCarNames(String[] carNames) { + if (carNames == null || carNames.length == 0) { + throw new IllegalArgumentException("자동차 이름 목록이 비어있습니다."); + } + for (String name : carNames) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("자동차 이름은 비어있을 수 없습니다."); + } + } + } + + private void validateRoundCount(int roundCount) { + if (roundCount < 1) { + throw new IllegalArgumentException("시도 횟수는 1 이상이어야 합니다."); + } + } + + public String[] getCarNames() { + return carNames; + } + public int getCarCount() { - return carCount; + return carNames.length; } public int getRoundCount() { diff --git a/src/main/java/InputView.java b/src/main/java/InputView.java index 3858ddbb538..fb593125348 100644 --- a/src/main/java/InputView.java +++ b/src/main/java/InputView.java @@ -8,9 +8,15 @@ public InputView() { } public GameSettings getGameSettings() { - int carCount = promptInt("자동차 대수는 몇 대 인가요?"); + String inputCarNames = prompt("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); + String[] carNames = inputCarNames.split(","); int roundCount = promptInt("시도할 회수는 몇 회 인가요?"); - return new GameSettings(carCount, roundCount); + return new GameSettings(carNames, roundCount); + } + + private String prompt(String message) { + System.out.println(message); + return scanner.nextLine(); } private int promptInt(String message) { diff --git a/src/main/java/Race.java b/src/main/java/Race.java index ed061d05894..e6ac71c3ee8 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -10,14 +10,10 @@ public class Race { private int currentRound = 0; public Race(GameSettings settings) { - if (settings.getCarCount() < 1 || settings.getRoundCount() < 1) { - throw new IllegalArgumentException("Invalid game settings: " + settings); - } - this.totalRounds = settings.getRoundCount(); this.cars = new ArrayList<>(); - for (int i = 0; i < settings.getCarCount(); i++) { - this.cars.add(new Car()); + for (String carName : settings.getCarNames()) { + this.cars.add(new Car(carName)); } } diff --git a/src/test/java/CarTest.java b/src/test/java/CarTest.java index fd02795c76d..2c2f5df8a76 100644 --- a/src/test/java/CarTest.java +++ b/src/test/java/CarTest.java @@ -12,10 +12,7 @@ class CarTest { @Test @DisplayName("자동차는 이름을 가질 수 있다.") void carHasName() { - Car car = new Car(); - assertThat(car.getName()).isEqualTo(Car.TEMP_CAR_NAME); - - car = new Car("MyCar"); + Car car = new Car("MyCar"); assertThat(car.getName()).isEqualTo("MyCar"); } diff --git a/src/test/java/GameSettingsTest.java b/src/test/java/GameSettingsTest.java new file mode 100644 index 00000000000..d2ed7672682 --- /dev/null +++ b/src/test/java/GameSettingsTest.java @@ -0,0 +1,46 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GameSettingsTest { + + @Test + @DisplayName("자동차 이름 목록이 비어있으면 에러가 발생한다") + void emptyCarNamesListThrowsError() { + assertThatThrownBy(() -> new GameSettings(new String[0], 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름 목록이 비어있습니다."); + + assertThatThrownBy(() -> new GameSettings(null, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름 목록이 비어있습니다."); + } + + @Test + @DisplayName("자동차 이름이 비어있으면 에러가 발생한다") + void emptyCarNameThrowsError() { + String[] carNamesWithBlank = {"car1", "", "car3"}; + assertThatThrownBy(() -> new GameSettings(carNamesWithBlank, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름은 비어있을 수 없습니다."); + + String[] carNamesWithNull = {"car1", null, "car3"}; + assertThatThrownBy(() -> new GameSettings(carNamesWithNull, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름은 비어있을 수 없습니다."); + } + + @Test + @DisplayName("시도 횟수가 1 미만이면 에러가 발생한다") + void roundCountLessThanOneThrowsError() { + String[] carNames = {"car1", "car2", "car3"}; + assertThatThrownBy(() -> new GameSettings(carNames, 0)).isInstanceOf(IllegalArgumentException.class).hasMessage("시도 횟수는 1 이상이어야 합니다."); + + assertThatThrownBy(() -> new GameSettings(carNames, -1)).isInstanceOf(IllegalArgumentException.class).hasMessage("시도 횟수는 1 이상이어야 합니다."); + } + + @Test + @DisplayName("유효한 입력으로 GameSettings가 생성된다") + void validInputCreatesGameSettings() { + String[] carNames = {"car1", "car2", "car3"}; + GameSettings settings = new GameSettings(carNames, 3); + + assertThat(settings.getCarNames()).isEqualTo(carNames); + assertThat(settings.getCarCount()).isEqualTo(3); + assertThat(settings.getRoundCount()).isEqualTo(3); + } +} \ No newline at end of file diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index d6b6e02c082..ac50043edcb 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -1,6 +1,5 @@ import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -9,47 +8,20 @@ class RaceTest { @Test - @DisplayName("자동차 개수는 양수여야 한다.") - void negativeOrZeroCarCountInputReturnsError() { - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(-1, 3)); - }); - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(0, 3)); - }); - - Race race = new Race(new GameSettings(1, 3)); - assertThat(race.getCarStatuses()).hasSize(1); - } - - @Test - @DisplayName("경주 횟수는 양수여야 한다.") - void negativeOrZeroRaceCountInputReturnsError() { - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(5, -1)); - }); - assertThrows(IllegalArgumentException.class, () -> { - new Race(new GameSettings(5, 0)); - }); - - assertDoesNotThrow(() -> { - new Race(new GameSettings(5, 1)); - }); - } - - @Test - @DisplayName("경기 시작시 자동차의 위치는 0이다.") + @DisplayName("경기 시작시 자동차의 위치는 0이다") void carPositionsAtStartAreZero() { - Race race = new Race(new GameSettings(5, 3)); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); for (CarStatus carStatus : race.getCarStatuses()) { assertThat(carStatus.getPosition()).isZero(); } } @Test - @DisplayName("한 라운드가 진행되면 자동차의 위치는 기존 위치이거나, 기존 위치 + 1이다.") + @DisplayName("한 라운드가 진행되면 자동차의 위치는 기존 위치이거나, 기존 위치 + 1이다") void carPositionsAfterOneRoundAreEitherSameOrIncremented() { - Race race = new Race(new GameSettings(5, 3)); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); List<CarStatus> initialStatuses = race.getCarStatuses(); race.runRound(); @@ -61,12 +33,13 @@ void carPositionsAfterOneRoundAreEitherSameOrIncremented() { } @Test - @DisplayName("전체 라운드를 넘어가면 에러가 발생한다.") + @DisplayName("전체 라운드를 넘어가면 에러가 발생한다") void exceedingTotalRoundsThrowsError() { - Race race = new Race(new GameSettings(5, 3)); + String[] carNames = {"car1", "car2", "car3", "car4", "car5"}; + Race race = new Race(new GameSettings(carNames, 3)); for (int i = 0; i < 3; i++) { race.runRound(); } - assertThrows(IllegalStateException.class, race::runRound); + assertThatThrownBy(race::runRound).isInstanceOf(IllegalStateException.class).hasMessage("Race has already finished"); } } From 06ccfadb3c4435a0e0906358e1ae344b42ac4800 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 17:14:40 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat=20:=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=20=20=20-=20[x]=20`Race`=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=EC=97=90=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20=20=20=20-=20[x]=20`getWinners`=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20=20=20=20=20-=20[x]=20?= =?UTF-8?q?=EA=B0=80=EC=9E=A5=20=EB=A9=80=EB=A6=AC=20=EA=B0=84=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=B0=A8=EB=93=A4=20=EC=B0=BE=EA=B8=B0=20=20=20=20-?= =?UTF-8?q?=20[x]=20`ResultView`=EC=97=90=20=EC=9A=B0=EC=8A=B9=EC=9E=90=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=20=20=20-=20[x]=20`Game`=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=EC=A0=95=20=20=20=20=20=20-=20[x]=20?= =?UTF-8?q?=EA=B2=BD=EC=A3=BC=20=EC=A2=85=EB=A3=8C=20=ED=9B=84=20=EC=9A=B0?= =?UTF-8?q?=EC=8A=B9=EC=9E=90=20=EC=B6=9C=EB=A0=A5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 12 ++++---- src/main/java/Car.java | 7 +++++ src/main/java/Game.java | 1 + src/main/java/Race.java | 56 ++++++++++++++++++++++++++++++++--- src/main/java/ResultView.java | 9 ++++++ src/test/java/RaceTest.java | 50 +++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 58a3e485f6b..0e14e04aa0c 100644 --- a/README.md +++ b/README.md @@ -88,12 +88,12 @@ ResultView : 결과를 출력 4. **우승자 판정** - - [ ] `Race` 클래스에 우승자 판정 로직 추가 - - [ ] `getWinners` 메소드 추가 - - [ ] 가장 멀리 간 자동차들 찾기 - - [ ] `ResultView`에 우승자 출력 메소드 추가 - - [ ] `Game` 클래스 수정 - - [ ] 경주 종료 후 우승자 출력 로직 추가 + - [x] `Race` 클래스에 우승자 판정 로직 추가 + - [x] `getWinners` 메소드 추가 + - [x] 가장 멀리 간 자동차들 찾기 + - [x] `ResultView`에 우승자 출력 메소드 추가 + - [x] `Game` 클래스 수정 + - [x] 경주 종료 후 우승자 출력 로직 추가 5. **테스트 추가** diff --git a/src/main/java/Car.java b/src/main/java/Car.java index 1b454d7663e..1778e8f88fa 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -9,6 +9,13 @@ public Car(String name) { this.name = name.trim(); } + // 테스트용 생성자 + Car(String name, int position) { + validateName(name); + this.name = name.trim(); + this.position = position; + } + private void validateName(String name) { if (name == null || name.trim().isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); diff --git a/src/main/java/Game.java b/src/main/java/Game.java index 642d47e3ea3..c66c6d04fab 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -17,6 +17,7 @@ public void start() { race.runRound(); } resultView.presentCars(race.getCarStatuses()); + resultView.presentWinners(race.getWinners()); } public static void main(String[] args) { diff --git a/src/main/java/Race.java b/src/main/java/Race.java index e6ac71c3ee8..aecfe9ba141 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -17,6 +17,13 @@ public Race(GameSettings settings) { } } + // 테스트용 생성자 + Race(List<Car> cars, int totalRounds) { + this.cars = new ArrayList<>(cars); + this.totalRounds = totalRounds; + this.currentRound = totalRounds; // 경주가 끝난 상태로 설정 + } + public List<CarStatus> getCarStatuses() { List<CarStatus> statuses = new ArrayList<>(); for (Car car : cars) { @@ -26,10 +33,7 @@ public List<CarStatus> getCarStatuses() { } public void runRound() { - if (!isRaceInProgress()) { - throw new IllegalStateException("Race has already finished"); - } - + validateRaceInProgress(); for (Car car : cars) { car.move(random.nextInt(10)); } @@ -39,4 +43,48 @@ public void runRound() { public boolean isRaceInProgress() { return currentRound < totalRounds; } + + private void validateRaceInProgress() { + if (!isRaceInProgress()) { + throw new IllegalStateException("Race has already finished"); + } + } + + private void validateRaceFinished() { + if (isRaceInProgress()) { + throw new IllegalStateException("Race is still in progress"); + } + } + + private int findMaxPosition() { + int maxPosition = 0; + for (Car car : cars) { + maxPosition = Math.max(maxPosition, car.getPosition()); + } + return maxPosition; + } + + private boolean isWinner(Car car, int maxPosition) { + return car.getPosition() == maxPosition; + } + + private List<CarStatus> findWinnersWithPosition(int maxPosition) { + List<CarStatus> winners = new ArrayList<>(); + for (Car car : cars) { + addWinnerIfQualified(winners, car, maxPosition); + } + return winners; + } + + private void addWinnerIfQualified(List<CarStatus> winners, Car car, int maxPosition) { + if (isWinner(car, maxPosition)) { + winners.add(new CarStatus(car)); + } + } + + public List<CarStatus> getWinners() { + validateRaceFinished(); + int maxPosition = findMaxPosition(); + return findWinnersWithPosition(maxPosition); + } } diff --git a/src/main/java/ResultView.java b/src/main/java/ResultView.java index f1a27b1658e..25874674706 100644 --- a/src/main/java/ResultView.java +++ b/src/main/java/ResultView.java @@ -1,4 +1,5 @@ import java.util.List; +import java.util.StringJoiner; public class ResultView { @@ -14,4 +15,12 @@ public void presentCars(List<CarStatus> cars) { } System.out.println(); } + + public void presentWinners(List<CarStatus> winners) { + StringJoiner joiner = new StringJoiner(", "); + for (CarStatus winner : winners) { + joiner.add(winner.getName()); + } + System.out.println(joiner.toString() + "가 최종 우승했습니다."); + } } diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index ac50043edcb..743975e4b19 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -42,4 +42,54 @@ void exceedingTotalRoundsThrowsError() { } assertThatThrownBy(race::runRound).isInstanceOf(IllegalStateException.class).hasMessage("Race has already finished"); } + + @Test + @DisplayName("경주가 진행 중일 때 우승자를 조회하면 예외가 발생한다") + void getWinnersBeforeRaceFinishThrowsError() { + String[] carNames = {"car1", "car2"}; + Race race = new Race(new GameSettings(carNames, 3)); + race.runRound(); // 1라운드만 진행 + + assertThatThrownBy(race::getWinners) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Race is still in progress"); + } + + @Test + @DisplayName("단독 우승자가 있는 경우 해당 자동차만 반환한다") + void getSingleWinner() { + // given + List<Car> cars = List.of( + new Car("car1", 5), + new Car("car2", 3), + new Car("car3", 4) + ); + Race race = new Race(cars, 1); + + // when + List<CarStatus> winners = race.getWinners(); + + // then + assertThat(winners).hasSize(1); + assertThat(winners.get(0).getName()).isEqualTo("car1"); + } + + @Test + @DisplayName("공동 우승자가 있는 경우 모든 우승자를 반환한다") + void getMultipleWinners() { + // given + List<Car> cars = List.of( + new Car("car1", 5), + new Car("car2", 5), + new Car("car3", 3) + ); + Race race = new Race(cars, 1); + + // when + List<CarStatus> winners = race.getWinners(); + + // then + assertThat(winners).hasSize(2); + assertThat(winners).extracting("name").containsExactlyInAnyOrder("car1", "car2"); + } } From 778c6c37413593a83bc9bb0893015386107dc378 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 17:44:46 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=9A=A9=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 23 +++++++-------- src/main/java/.gitkeep | 0 src/main/java/Car.java | 16 ++--------- src/main/java/MoveStrategy.java | 4 +++ src/main/java/Race.java | 16 +++++------ src/main/java/RandomMoveStrategy.java | 11 +++++++ src/test/java/CarTest.java | 17 +++-------- src/test/java/RaceTest.java | 41 +++++++++++++++++++-------- 8 files changed, 67 insertions(+), 61 deletions(-) delete mode 100644 src/main/java/.gitkeep create mode 100644 src/main/java/MoveStrategy.java create mode 100644 src/main/java/RandomMoveStrategy.java diff --git a/README.md b/README.md index 0e14e04aa0c..b3a3f436430 100644 --- a/README.md +++ b/README.md @@ -95,19 +95,16 @@ ResultView : 결과를 출력 - [x] `Game` 클래스 수정 - [x] 경주 종료 후 우승자 출력 로직 추가 -5. **테스트 추가** - - - [ ] `RaceTest`에 우승자 판정 테스트 추가 - - [ ] 단일 우승자 케이스 - - [ ] 다중 우승자 케이스 - - [ ] `InputViewTest` 추가 - - [ ] 자동차 이름 입력 테스트 - - [ ] 잘못된 입력 처리 테스트 - -6. **리팩토링 고려사항** - - [ ] `Car` 클래스에 `Position` 값 객체 도입 검토 - - [ ] `CarName` 값 객체 도입 검토 - - [ ] `CarStatus` DTO 도입 검토 (이름과 위치를 함께 전달하기 위해) +### 개발 구조 + +- **Game**: 게임의 전체 생명주기 관리 및 사용자 인터랙션 조정 +- **Race**: 경주 진행 상태 관리, 자동차 이동 처리, 우승자 판정 +- **GameSettings**: 게임 설정값(자동차 이름, 라운드 수) 검증 및 관리 +- **Car**: 자동차의 기본 속성(이름, 위치) 관리 +- **InputView**: 사용자 입력 처리 및 검증 +- **ResultView**: 게임 진행 상태 및 결과 출력 +- **CarStatus**: 자동차의 현재 상태를 불변 객체로 표현 +- **MoveStrategy**: 자동차 이동 전략 정의 (RandomMoveStrategy, FixedMoveStrategy) ### 프로그래밍 요구사항 diff --git a/src/main/java/.gitkeep b/src/main/java/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/main/java/Car.java b/src/main/java/Car.java index 1778e8f88fa..b0159bb98a8 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -1,6 +1,5 @@ public class Car { - private static final int MOVEMENT_THRESHOLD = 4; private final String name; private int position = 0; @@ -9,13 +8,6 @@ public Car(String name) { this.name = name.trim(); } - // 테스트용 생성자 - Car(String name, int position) { - validateName(name); - this.name = name.trim(); - this.position = position; - } - private void validateName(String name) { if (name == null || name.trim().isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); @@ -25,12 +17,8 @@ private void validateName(String name) { } } - public void move(int seed) { - if (seed < 0 || seed > 9) { - throw new IllegalArgumentException("Invalid seed: " + seed); - } - - if (seed >= MOVEMENT_THRESHOLD) { + public void move(boolean shouldMove) { + if (shouldMove) { position++; } } diff --git a/src/main/java/MoveStrategy.java b/src/main/java/MoveStrategy.java new file mode 100644 index 00000000000..944284c6f5c --- /dev/null +++ b/src/main/java/MoveStrategy.java @@ -0,0 +1,4 @@ +@FunctionalInterface +public interface MoveStrategy { + boolean shouldMove(); +} \ No newline at end of file diff --git a/src/main/java/Race.java b/src/main/java/Race.java index aecfe9ba141..53e8fde9d81 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -4,24 +4,22 @@ public class Race { - private static final Random random = new Random(); + private final MoveStrategy moveStrategy; private final int totalRounds; private final List<Car> cars; private int currentRound = 0; public Race(GameSettings settings) { + this(settings, new RandomMoveStrategy()); + } + + public Race(GameSettings settings, MoveStrategy moveStrategy) { this.totalRounds = settings.getRoundCount(); this.cars = new ArrayList<>(); for (String carName : settings.getCarNames()) { this.cars.add(new Car(carName)); } - } - - // 테스트용 생성자 - Race(List<Car> cars, int totalRounds) { - this.cars = new ArrayList<>(cars); - this.totalRounds = totalRounds; - this.currentRound = totalRounds; // 경주가 끝난 상태로 설정 + this.moveStrategy = moveStrategy; } public List<CarStatus> getCarStatuses() { @@ -35,7 +33,7 @@ public List<CarStatus> getCarStatuses() { public void runRound() { validateRaceInProgress(); for (Car car : cars) { - car.move(random.nextInt(10)); + car.move(moveStrategy.shouldMove()); } currentRound++; } diff --git a/src/main/java/RandomMoveStrategy.java b/src/main/java/RandomMoveStrategy.java new file mode 100644 index 00000000000..727eb75d362 --- /dev/null +++ b/src/main/java/RandomMoveStrategy.java @@ -0,0 +1,11 @@ +import java.util.Random; + +public class RandomMoveStrategy implements MoveStrategy { + private static final int MOVE_THRESHOLD = 4; + private final Random random = new Random(); + + @Override + public boolean shouldMove() { + return random.nextInt(10) >= MOVE_THRESHOLD; + } +} \ No newline at end of file diff --git a/src/test/java/CarTest.java b/src/test/java/CarTest.java index 2c2f5df8a76..8f34f00954a 100644 --- a/src/test/java/CarTest.java +++ b/src/test/java/CarTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; class CarTest { @@ -35,19 +34,11 @@ void carNameCannotExceed5Characters() { } @ParameterizedTest - @DisplayName("random값이 4 이상일 경우에 자동차의 위치는 1 추가되고, 4 미만일 경우 위치가 변하지 않는다") - @CsvSource({"0, 0", "1, 0", "2, 0", "3, 0", "4, 1", "5, 1", "6, 1", "7, 1", "8, 1", "9, 1"}) - void carMovesAccordingToRandomValue(int seed, int expectedPosition) { + @DisplayName("shouldMove가 true일 경우 자동차의 위치는 1 추가되고, false일 경우 위치가 변하지 않는다") + @CsvSource({"false, 0", "true, 1"}) + void carMovesAccordingToShouldMove(boolean shouldMove, int expectedPosition) { Car car = new Car("MyCar"); - car.move(seed); + car.move(shouldMove); assertThat(car.getPosition()).isEqualTo(expectedPosition); } - - @ParameterizedTest - @DisplayName("이동을 위한 숫자는 0에서 9 사이여야 한다") - @ValueSource(ints = {-1, 10}) - void carMoveVariableIsBetween0And9(int invalidSeed) { - Car car = new Car("MyCar"); - assertThatThrownBy(() -> car.move(invalidSeed)).isInstanceOf(IllegalArgumentException.class).hasMessage("Invalid seed: " + invalidSeed); - } } diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index 743975e4b19..1c841910cb2 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -59,14 +59,14 @@ void getWinnersBeforeRaceFinishThrowsError() { @DisplayName("단독 우승자가 있는 경우 해당 자동차만 반환한다") void getSingleWinner() { // given - List<Car> cars = List.of( - new Car("car1", 5), - new Car("car2", 3), - new Car("car3", 4) - ); - Race race = new Race(cars, 1); + String[] carNames = {"car1", "car2", "car3"}; + GameSettings settings = new GameSettings(carNames, 3); + Race race = new Race(settings, new FixedMoveStrategy(new boolean[]{true, false, false})); // when + race.runRound(); + race.runRound(); + race.runRound(); List<CarStatus> winners = race.getWinners(); // then @@ -78,18 +78,35 @@ void getSingleWinner() { @DisplayName("공동 우승자가 있는 경우 모든 우승자를 반환한다") void getMultipleWinners() { // given - List<Car> cars = List.of( - new Car("car1", 5), - new Car("car2", 5), - new Car("car3", 3) - ); - Race race = new Race(cars, 1); + String[] carNames = {"car1", "car2", "car3"}; + GameSettings settings = new GameSettings(carNames, 3); + Race race = new Race(settings, new FixedMoveStrategy(new boolean[]{true, true, false})); // when + race.runRound(); + race.runRound(); + race.runRound(); List<CarStatus> winners = race.getWinners(); // then assertThat(winners).hasSize(2); assertThat(winners).extracting("name").containsExactlyInAnyOrder("car1", "car2"); } + + private static class FixedMoveStrategy implements MoveStrategy { + private final boolean[] moves; + private int index = 0; + + FixedMoveStrategy(boolean[] moves) { + this.moves = moves; + } + + @Override + public boolean shouldMove() { + if (index >= moves.length) { + index = 0; + } + return moves[index++]; + } + } } From 63dff0461e0347812b2e9883681d86f1f712f9e8 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Thu, 20 Mar 2025 17:49:21 +0900 Subject: [PATCH 7/9] =?UTF-8?q?refactor=20:=20=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/CarStatus.java | 1 + src/main/java/Game.java | 11 ++++++----- src/main/java/InputView.java | 4 +++- src/main/java/MoveStrategy.java | 1 + src/main/java/Race.java | 1 - src/main/java/RandomMoveStrategy.java | 4 +++- src/test/java/RaceTest.java | 5 +++-- 7 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/CarStatus.java b/src/main/java/CarStatus.java index 1ba73da4c8f..41f0555525f 100644 --- a/src/main/java/CarStatus.java +++ b/src/main/java/CarStatus.java @@ -1,4 +1,5 @@ public class CarStatus { + private final String name; private final int position; diff --git a/src/main/java/Game.java b/src/main/java/Game.java index c66c6d04fab..6338bf72b85 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -1,4 +1,5 @@ public class Game { + private final InputView inputView; private final ResultView resultView; @@ -7,6 +8,11 @@ public Game(InputView inputView, ResultView resultView) { this.resultView = resultView; } + public static void main(String[] args) { + Game game = new Game(new InputView(), new ResultView()); + game.start(); + } + public void start() { GameSettings settings = inputView.getGameSettings(); resultView.presentStartMessage(); @@ -19,9 +25,4 @@ public void start() { resultView.presentCars(race.getCarStatuses()); resultView.presentWinners(race.getWinners()); } - - public static void main(String[] args) { - Game game = new Game(new InputView(), new ResultView()); - game.start(); - } } diff --git a/src/main/java/InputView.java b/src/main/java/InputView.java index fb593125348..fddb499cc3d 100644 --- a/src/main/java/InputView.java +++ b/src/main/java/InputView.java @@ -1,6 +1,8 @@ import java.util.Scanner; public class InputView { + + public static final String CAR_NAME_DELIMITER = ","; private final Scanner scanner; public InputView() { @@ -9,7 +11,7 @@ public InputView() { public GameSettings getGameSettings() { String inputCarNames = prompt("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); - String[] carNames = inputCarNames.split(","); + String[] carNames = inputCarNames.split(CAR_NAME_DELIMITER); int roundCount = promptInt("시도할 회수는 몇 회 인가요?"); return new GameSettings(carNames, roundCount); } diff --git a/src/main/java/MoveStrategy.java b/src/main/java/MoveStrategy.java index 944284c6f5c..ad9e1fd17d9 100644 --- a/src/main/java/MoveStrategy.java +++ b/src/main/java/MoveStrategy.java @@ -1,4 +1,5 @@ @FunctionalInterface public interface MoveStrategy { + boolean shouldMove(); } \ No newline at end of file diff --git a/src/main/java/Race.java b/src/main/java/Race.java index 53e8fde9d81..420ba8524ae 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -1,6 +1,5 @@ import java.util.ArrayList; import java.util.List; -import java.util.Random; public class Race { diff --git a/src/main/java/RandomMoveStrategy.java b/src/main/java/RandomMoveStrategy.java index 727eb75d362..f639389ca85 100644 --- a/src/main/java/RandomMoveStrategy.java +++ b/src/main/java/RandomMoveStrategy.java @@ -1,11 +1,13 @@ import java.util.Random; public class RandomMoveStrategy implements MoveStrategy { + + public static final int RANDOM_NUMBER_RANGE = 10; private static final int MOVE_THRESHOLD = 4; private final Random random = new Random(); @Override public boolean shouldMove() { - return random.nextInt(10) >= MOVE_THRESHOLD; + return random.nextInt(RANDOM_NUMBER_RANGE) >= MOVE_THRESHOLD; } } \ No newline at end of file diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index 1c841910cb2..b8f4c928366 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -51,8 +51,8 @@ void getWinnersBeforeRaceFinishThrowsError() { race.runRound(); // 1라운드만 진행 assertThatThrownBy(race::getWinners) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Race is still in progress"); + .isInstanceOf(IllegalStateException.class) + .hasMessage("Race is still in progress"); } @Test @@ -94,6 +94,7 @@ void getMultipleWinners() { } private static class FixedMoveStrategy implements MoveStrategy { + private final boolean[] moves; private int index = 0; From 9e090bdd87f2126bd6911f7bbbabde249ed18093 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Mon, 24 Mar 2025 19:05:01 +0900 Subject: [PATCH 8/9] =?UTF-8?q?refactor=20:=20PR=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20-=20List<Car>=20->=20Cars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/Car.java | 6 ++- src/main/java/CarStatus.java | 21 ++++++++++ src/main/java/Cars.java | 58 ++++++++++++++++++++++++++ src/main/java/Game.java | 29 +++++-------- src/main/java/GameSettings.java | 2 +- src/main/java/InputView.java | 15 ++++--- src/main/java/Race.java | 47 +++------------------ src/main/java/RandomMoveStrategy.java | 2 +- src/main/java/ResultView.java | 9 ++-- src/test/java/CarsTest.java | 59 +++++++++++++++++++++++++++ src/test/java/RaceTest.java | 56 ------------------------- 11 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 src/main/java/Cars.java create mode 100644 src/test/java/CarsTest.java diff --git a/src/main/java/Car.java b/src/main/java/Car.java index b0159bb98a8..d989069567d 100644 --- a/src/main/java/Car.java +++ b/src/main/java/Car.java @@ -9,7 +9,7 @@ public Car(String name) { } private void validateName(String name) { - if (name == null || name.trim().isBlank()) { + if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be blank"); } if (name.length() > 5) { @@ -30,4 +30,8 @@ public String getName() { public int getPosition() { return position; } + + public boolean isWinner(int maxPosition) { + return position == maxPosition; + } } diff --git a/src/main/java/CarStatus.java b/src/main/java/CarStatus.java index 41f0555525f..d511541e561 100644 --- a/src/main/java/CarStatus.java +++ b/src/main/java/CarStatus.java @@ -1,3 +1,5 @@ +import java.util.Objects; + public class CarStatus { private final String name; @@ -8,6 +10,12 @@ public CarStatus(Car car) { this.position = car.getPosition(); } + // 테스트용 생성자 + public CarStatus(String name, int position) { + this.name = name; + this.position = position; + } + public String getName() { return name; } @@ -15,4 +23,17 @@ public String getName() { public int getPosition() { return position; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CarStatus carStatus = (CarStatus) o; + return position == carStatus.position && Objects.equals(name, carStatus.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, position); + } } \ No newline at end of file diff --git a/src/main/java/Cars.java b/src/main/java/Cars.java new file mode 100644 index 00000000000..4be56846bbd --- /dev/null +++ b/src/main/java/Cars.java @@ -0,0 +1,58 @@ +import java.util.ArrayList; +import java.util.List; + +public class Cars { + private final List<Car> cars; + + private Cars(List<Car> cars) { + validateCars(cars); + this.cars = new ArrayList<>(cars); + } + + public static Cars fromNames(String[] carNames) { + List<Car> carList = new ArrayList<>(); + for (String carName : carNames) { + carList.add(new Car(carName)); + } + return new Cars(carList); + } + + private void validateCars(List<Car> cars) { + if (cars == null || cars.isEmpty()) { + throw new IllegalArgumentException("자동차 목록이 비어있습니다."); + } + } + + public List<CarStatus> getCarStatuses() { + List<CarStatus> statuses = new ArrayList<>(); + for (Car car : cars) { + statuses.add(new CarStatus(car)); + } + return statuses; + } + + public void moveAll(MoveStrategy moveStrategy) { + for (Car car : cars) { + car.move(moveStrategy.shouldMove()); + } + } + + public int findMaxPosition() { + int maxPosition = 0; + for (Car car : cars) { + maxPosition = Math.max(maxPosition, car.getPosition()); + } + return maxPosition; + } + + public List<CarStatus> findWinners() { + int maxPosition = findMaxPosition(); + List<CarStatus> winners = new ArrayList<>(); + for (Car car : cars) { + if (car.isWinner(maxPosition)) { + winners.add(new CarStatus(car)); + } + } + return winners; + } +} \ No newline at end of file diff --git a/src/main/java/Game.java b/src/main/java/Game.java index 6338bf72b85..feaf8e05520 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -1,28 +1,19 @@ public class Game { - - private final InputView inputView; - private final ResultView resultView; - - public Game(InputView inputView, ResultView resultView) { - this.inputView = inputView; - this.resultView = resultView; - } - - public static void main(String[] args) { - Game game = new Game(new InputView(), new ResultView()); - game.start(); - } - public void start() { - GameSettings settings = inputView.getGameSettings(); - resultView.presentStartMessage(); + GameSettings settings = InputView.getGameSettings(); + ResultView.presentStartMessage(); Race race = new Race(settings); while (race.isRaceInProgress()) { - resultView.presentCars(race.getCarStatuses()); + ResultView.presentCars(race.getCarStatuses()); race.runRound(); } - resultView.presentCars(race.getCarStatuses()); - resultView.presentWinners(race.getWinners()); + ResultView.presentCars(race.getCarStatuses()); + ResultView.presentWinners(race.getWinners()); + } + + public static void main(String[] args) { + Game game = new Game(); + game.start(); } } diff --git a/src/main/java/GameSettings.java b/src/main/java/GameSettings.java index 13fc0509e14..49cfa1e38f7 100644 --- a/src/main/java/GameSettings.java +++ b/src/main/java/GameSettings.java @@ -16,7 +16,7 @@ private void validateCarNames(String[] carNames) { throw new IllegalArgumentException("자동차 이름 목록이 비어있습니다."); } for (String name : carNames) { - if (name == null || name.trim().isEmpty()) { + if (name == null || name.isBlank()) { throw new IllegalArgumentException("자동차 이름은 비어있을 수 없습니다."); } } diff --git a/src/main/java/InputView.java b/src/main/java/InputView.java index fddb499cc3d..0cf46245d57 100644 --- a/src/main/java/InputView.java +++ b/src/main/java/InputView.java @@ -1,27 +1,26 @@ import java.util.Scanner; public class InputView { + private static final String CAR_NAME_DELIMITER = ","; + private static final Scanner scanner = new Scanner(System.in); - public static final String CAR_NAME_DELIMITER = ","; - private final Scanner scanner; - - public InputView() { - this.scanner = new Scanner(System.in); + private InputView() { + // private 생성자로 인스턴스화 방지 } - public GameSettings getGameSettings() { + public static GameSettings getGameSettings() { String inputCarNames = prompt("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); String[] carNames = inputCarNames.split(CAR_NAME_DELIMITER); int roundCount = promptInt("시도할 회수는 몇 회 인가요?"); return new GameSettings(carNames, roundCount); } - private String prompt(String message) { + private static String prompt(String message) { System.out.println(message); return scanner.nextLine(); } - private int promptInt(String message) { + private static int promptInt(String message) { System.out.println(message); while (!scanner.hasNextInt()) { System.out.println("That's not a valid number!"); diff --git a/src/main/java/Race.java b/src/main/java/Race.java index 420ba8524ae..27eb70b6fb4 100644 --- a/src/main/java/Race.java +++ b/src/main/java/Race.java @@ -1,11 +1,10 @@ -import java.util.ArrayList; import java.util.List; public class Race { private final MoveStrategy moveStrategy; private final int totalRounds; - private final List<Car> cars; + private final Cars cars; private int currentRound = 0; public Race(GameSettings settings) { @@ -14,26 +13,17 @@ public Race(GameSettings settings) { public Race(GameSettings settings, MoveStrategy moveStrategy) { this.totalRounds = settings.getRoundCount(); - this.cars = new ArrayList<>(); - for (String carName : settings.getCarNames()) { - this.cars.add(new Car(carName)); - } + this.cars = Cars.fromNames(settings.getCarNames()); this.moveStrategy = moveStrategy; } public List<CarStatus> getCarStatuses() { - List<CarStatus> statuses = new ArrayList<>(); - for (Car car : cars) { - statuses.add(new CarStatus(car)); - } - return statuses; + return cars.getCarStatuses(); } public void runRound() { validateRaceInProgress(); - for (Car car : cars) { - car.move(moveStrategy.shouldMove()); - } + cars.moveAll(moveStrategy); currentRound++; } @@ -53,35 +43,8 @@ private void validateRaceFinished() { } } - private int findMaxPosition() { - int maxPosition = 0; - for (Car car : cars) { - maxPosition = Math.max(maxPosition, car.getPosition()); - } - return maxPosition; - } - - private boolean isWinner(Car car, int maxPosition) { - return car.getPosition() == maxPosition; - } - - private List<CarStatus> findWinnersWithPosition(int maxPosition) { - List<CarStatus> winners = new ArrayList<>(); - for (Car car : cars) { - addWinnerIfQualified(winners, car, maxPosition); - } - return winners; - } - - private void addWinnerIfQualified(List<CarStatus> winners, Car car, int maxPosition) { - if (isWinner(car, maxPosition)) { - winners.add(new CarStatus(car)); - } - } - public List<CarStatus> getWinners() { validateRaceFinished(); - int maxPosition = findMaxPosition(); - return findWinnersWithPosition(maxPosition); + return cars.findWinners(); } } diff --git a/src/main/java/RandomMoveStrategy.java b/src/main/java/RandomMoveStrategy.java index f639389ca85..41d27af2199 100644 --- a/src/main/java/RandomMoveStrategy.java +++ b/src/main/java/RandomMoveStrategy.java @@ -2,7 +2,7 @@ public class RandomMoveStrategy implements MoveStrategy { - public static final int RANDOM_NUMBER_RANGE = 10; + private static final int RANDOM_NUMBER_RANGE = 10; private static final int MOVE_THRESHOLD = 4; private final Random random = new Random(); diff --git a/src/main/java/ResultView.java b/src/main/java/ResultView.java index 25874674706..a2cabe71814 100644 --- a/src/main/java/ResultView.java +++ b/src/main/java/ResultView.java @@ -2,12 +2,15 @@ import java.util.StringJoiner; public class ResultView { + private ResultView() { + // private 생성자로 인스턴스화 방지 + } - public void presentStartMessage() { + public static void presentStartMessage() { System.out.println("실행 결과"); } - public void presentCars(List<CarStatus> cars) { + public static void presentCars(List<CarStatus> cars) { for (CarStatus car : cars) { String positionIndicator = "-".repeat(car.getPosition() + 1); String output = String.format("%s : %s", car.getName(), positionIndicator); @@ -16,7 +19,7 @@ public void presentCars(List<CarStatus> cars) { System.out.println(); } - public void presentWinners(List<CarStatus> winners) { + public static void presentWinners(List<CarStatus> winners) { StringJoiner joiner = new StringJoiner(", "); for (CarStatus winner : winners) { joiner.add(winner.getName()); diff --git a/src/test/java/CarsTest.java b/src/test/java/CarsTest.java new file mode 100644 index 00000000000..43f089c0e1b --- /dev/null +++ b/src/test/java/CarsTest.java @@ -0,0 +1,59 @@ +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CarsTest { + + @Test + @DisplayName("단독 우승자가 있는 경우 해당 자동차만 반환한다") + void getSingleWinner() { + // given + Cars cars = createCars("car1", "car2", "car3"); + cars.moveAll(new FixedMoveStrategy(new boolean[]{true, false, false})); + + // then + assertThat(cars.findWinners()).hasSize(1); + assertThat(cars.findWinners()).contains(createCarStatus("car1", 1)); + } + + @Test + @DisplayName("공동 우승자가 있는 경우 모든 우승자를 반환한다") + void getMultipleWinners() { + // given + Cars cars = createCars("car1", "car2", "car3"); + cars.moveAll(new FixedMoveStrategy(new boolean[]{true, true, false})); + + // then + assertThat(cars.findWinners()).hasSize(2); + assertThat(cars.findWinners()).contains( + createCarStatus("car1", 1), + createCarStatus("car2", 1) + ); + } + + private static class FixedMoveStrategy implements MoveStrategy { + private final boolean[] moves; + private int index = 0; + + FixedMoveStrategy(boolean[] moves) { + this.moves = moves; + } + + @Override + public boolean shouldMove() { + if (index >= moves.length) { + index = 0; + } + return moves[index++]; + } + } + + private Cars createCars(String... names) { + return Cars.fromNames(names); + } + + private CarStatus createCarStatus(String name, int position) { + return new CarStatus(name, position); + } +} diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index b8f4c928366..a500b41beb8 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -54,60 +54,4 @@ void getWinnersBeforeRaceFinishThrowsError() { .isInstanceOf(IllegalStateException.class) .hasMessage("Race is still in progress"); } - - @Test - @DisplayName("단독 우승자가 있는 경우 해당 자동차만 반환한다") - void getSingleWinner() { - // given - String[] carNames = {"car1", "car2", "car3"}; - GameSettings settings = new GameSettings(carNames, 3); - Race race = new Race(settings, new FixedMoveStrategy(new boolean[]{true, false, false})); - - // when - race.runRound(); - race.runRound(); - race.runRound(); - List<CarStatus> winners = race.getWinners(); - - // then - assertThat(winners).hasSize(1); - assertThat(winners.get(0).getName()).isEqualTo("car1"); - } - - @Test - @DisplayName("공동 우승자가 있는 경우 모든 우승자를 반환한다") - void getMultipleWinners() { - // given - String[] carNames = {"car1", "car2", "car3"}; - GameSettings settings = new GameSettings(carNames, 3); - Race race = new Race(settings, new FixedMoveStrategy(new boolean[]{true, true, false})); - - // when - race.runRound(); - race.runRound(); - race.runRound(); - List<CarStatus> winners = race.getWinners(); - - // then - assertThat(winners).hasSize(2); - assertThat(winners).extracting("name").containsExactlyInAnyOrder("car1", "car2"); - } - - private static class FixedMoveStrategy implements MoveStrategy { - - private final boolean[] moves; - private int index = 0; - - FixedMoveStrategy(boolean[] moves) { - this.moves = moves; - } - - @Override - public boolean shouldMove() { - if (index >= moves.length) { - index = 0; - } - return moves[index++]; - } - } } From c85d2919dcde99754c1e8a07db0919584048b724 Mon Sep 17 00:00:00 2001 From: kjy2844 <kjy2844@kaist.ac.kr> Date: Mon, 24 Mar 2025 19:07:04 +0900 Subject: [PATCH 9/9] =?UTF-8?q?refactor=20:=20=ED=8F=AC=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/CarStatus.java | 10 +++++++--- src/main/java/Cars.java | 3 ++- src/main/java/Game.java | 11 ++++++----- src/main/java/GameSettings.java | 2 +- src/main/java/InputView.java | 1 + src/main/java/MoveStrategy.java | 2 +- src/main/java/RandomMoveStrategy.java | 2 +- src/main/java/ResultView.java | 3 ++- src/test/java/CarTest.java | 3 ++- src/test/java/CarsTest.java | 22 ++++++++++------------ src/test/java/GameSettingsTest.java | 8 +++++--- src/test/java/RaceTest.java | 4 +--- 12 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/main/java/CarStatus.java b/src/main/java/CarStatus.java index d511541e561..d0302ba48e4 100644 --- a/src/main/java/CarStatus.java +++ b/src/main/java/CarStatus.java @@ -26,8 +26,12 @@ public int getPosition() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } CarStatus carStatus = (CarStatus) o; return position == carStatus.position && Objects.equals(name, carStatus.name); } @@ -36,4 +40,4 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name, position); } -} \ No newline at end of file +} diff --git a/src/main/java/Cars.java b/src/main/java/Cars.java index 4be56846bbd..6a67f9fd04b 100644 --- a/src/main/java/Cars.java +++ b/src/main/java/Cars.java @@ -2,6 +2,7 @@ import java.util.List; public class Cars { + private final List<Car> cars; private Cars(List<Car> cars) { @@ -55,4 +56,4 @@ public List<CarStatus> findWinners() { } return winners; } -} \ No newline at end of file +} diff --git a/src/main/java/Game.java b/src/main/java/Game.java index feaf8e05520..a5b0c4793ef 100644 --- a/src/main/java/Game.java +++ b/src/main/java/Game.java @@ -1,4 +1,10 @@ public class Game { + + public static void main(String[] args) { + Game game = new Game(); + game.start(); + } + public void start() { GameSettings settings = InputView.getGameSettings(); ResultView.presentStartMessage(); @@ -11,9 +17,4 @@ public void start() { ResultView.presentCars(race.getCarStatuses()); ResultView.presentWinners(race.getWinners()); } - - public static void main(String[] args) { - Game game = new Game(); - game.start(); - } } diff --git a/src/main/java/GameSettings.java b/src/main/java/GameSettings.java index 49cfa1e38f7..1a457f068e9 100644 --- a/src/main/java/GameSettings.java +++ b/src/main/java/GameSettings.java @@ -39,4 +39,4 @@ public int getCarCount() { public int getRoundCount() { return roundCount; } -} \ No newline at end of file +} diff --git a/src/main/java/InputView.java b/src/main/java/InputView.java index 0cf46245d57..81afbe954db 100644 --- a/src/main/java/InputView.java +++ b/src/main/java/InputView.java @@ -1,6 +1,7 @@ import java.util.Scanner; public class InputView { + private static final String CAR_NAME_DELIMITER = ","; private static final Scanner scanner = new Scanner(System.in); diff --git a/src/main/java/MoveStrategy.java b/src/main/java/MoveStrategy.java index ad9e1fd17d9..82efa93af78 100644 --- a/src/main/java/MoveStrategy.java +++ b/src/main/java/MoveStrategy.java @@ -2,4 +2,4 @@ public interface MoveStrategy { boolean shouldMove(); -} \ No newline at end of file +} diff --git a/src/main/java/RandomMoveStrategy.java b/src/main/java/RandomMoveStrategy.java index 41d27af2199..87368c73a60 100644 --- a/src/main/java/RandomMoveStrategy.java +++ b/src/main/java/RandomMoveStrategy.java @@ -10,4 +10,4 @@ public class RandomMoveStrategy implements MoveStrategy { public boolean shouldMove() { return random.nextInt(RANDOM_NUMBER_RANGE) >= MOVE_THRESHOLD; } -} \ No newline at end of file +} diff --git a/src/main/java/ResultView.java b/src/main/java/ResultView.java index a2cabe71814..82f0332b796 100644 --- a/src/main/java/ResultView.java +++ b/src/main/java/ResultView.java @@ -2,6 +2,7 @@ import java.util.StringJoiner; public class ResultView { + private ResultView() { // private 생성자로 인스턴스화 방지 } @@ -24,6 +25,6 @@ public static void presentWinners(List<CarStatus> winners) { for (CarStatus winner : winners) { joiner.add(winner.getName()); } - System.out.println(joiner.toString() + "가 최종 우승했습니다."); + System.out.println(joiner + "가 최종 우승했습니다."); } } diff --git a/src/test/java/CarTest.java b/src/test/java/CarTest.java index 8f34f00954a..91a5417a416 100644 --- a/src/test/java/CarTest.java +++ b/src/test/java/CarTest.java @@ -30,7 +30,8 @@ void carNameCannotBeBlank() { @Test @DisplayName("자동차 이름은 5자를 초과할 수 없다") void carNameCannotExceed5Characters() { - assertThatThrownBy(() -> new Car("123456")).isInstanceOf(IllegalArgumentException.class).hasMessage("Name cannot be longer than 5 characters"); + assertThatThrownBy(() -> new Car("123456")).isInstanceOf(IllegalArgumentException.class) + .hasMessage("Name cannot be longer than 5 characters"); } @ParameterizedTest diff --git a/src/test/java/CarsTest.java b/src/test/java/CarsTest.java index 43f089c0e1b..772b0b3c87d 100644 --- a/src/test/java/CarsTest.java +++ b/src/test/java/CarsTest.java @@ -26,13 +26,19 @@ void getMultipleWinners() { // then assertThat(cars.findWinners()).hasSize(2); - assertThat(cars.findWinners()).contains( - createCarStatus("car1", 1), - createCarStatus("car2", 1) - ); + assertThat(cars.findWinners()).contains(createCarStatus("car1", 1), createCarStatus("car2", 1)); + } + + private Cars createCars(String... names) { + return Cars.fromNames(names); + } + + private CarStatus createCarStatus(String name, int position) { + return new CarStatus(name, position); } private static class FixedMoveStrategy implements MoveStrategy { + private final boolean[] moves; private int index = 0; @@ -48,12 +54,4 @@ public boolean shouldMove() { return moves[index++]; } } - - private Cars createCars(String... names) { - return Cars.fromNames(names); - } - - private CarStatus createCarStatus(String name, int position) { - return new CarStatus(name, position); - } } diff --git a/src/test/java/GameSettingsTest.java b/src/test/java/GameSettingsTest.java index d2ed7672682..535482d9fc2 100644 --- a/src/test/java/GameSettingsTest.java +++ b/src/test/java/GameSettingsTest.java @@ -18,10 +18,12 @@ void emptyCarNamesListThrowsError() { @DisplayName("자동차 이름이 비어있으면 에러가 발생한다") void emptyCarNameThrowsError() { String[] carNamesWithBlank = {"car1", "", "car3"}; - assertThatThrownBy(() -> new GameSettings(carNamesWithBlank, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름은 비어있을 수 없습니다."); + assertThatThrownBy(() -> new GameSettings(carNamesWithBlank, 3)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); String[] carNamesWithNull = {"car1", null, "car3"}; - assertThatThrownBy(() -> new GameSettings(carNamesWithNull, 3)).isInstanceOf(IllegalArgumentException.class).hasMessage("자동차 이름은 비어있을 수 없습니다."); + assertThatThrownBy(() -> new GameSettings(carNamesWithNull, 3)).isInstanceOf(IllegalArgumentException.class) + .hasMessage("자동차 이름은 비어있을 수 없습니다."); } @Test @@ -43,4 +45,4 @@ void validInputCreatesGameSettings() { assertThat(settings.getCarCount()).isEqualTo(3); assertThat(settings.getRoundCount()).isEqualTo(3); } -} \ No newline at end of file +} diff --git a/src/test/java/RaceTest.java b/src/test/java/RaceTest.java index a500b41beb8..e2ca442fd6c 100644 --- a/src/test/java/RaceTest.java +++ b/src/test/java/RaceTest.java @@ -50,8 +50,6 @@ void getWinnersBeforeRaceFinishThrowsError() { Race race = new Race(new GameSettings(carNames, 3)); race.runRound(); // 1라운드만 진행 - assertThatThrownBy(race::getWinners) - .isInstanceOf(IllegalStateException.class) - .hasMessage("Race is still in progress"); + assertThatThrownBy(race::getWinners).isInstanceOf(IllegalStateException.class).hasMessage("Race is still in progress"); } }