Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Step3 - 자동차 경주 #6042

Merged
merged 18 commits into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 69 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,69 @@
# 자동차 경주 게임
## 진행 방법
* 자동차 경주 게임 요구사항을 파악한다.
* 요구사항에 대한 구현을 완료한 후 자신의 github 아이디에 해당하는 브랜치에 Pull Request(이하 PR)를 통해 코드 리뷰 요청을 한다.
* 코드 리뷰 피드백에 대한 개선 작업을 하고 다시 PUSH한다.
* 모든 피드백을 완료하면 다음 단계를 도전하고 앞의 과정을 반복한다.

## 온라인 코드 리뷰 과정
* [텍스트와 이미지로 살펴보는 온라인 코드 리뷰 과정](https://github.com/next-step/nextstep-docs/tree/master/codereview)
# 3단계 자동차 경주

---
## 기능 요구사항
- [x] 초간단 자동차 경주 게임을 구현한다.
- [x] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
- [x] 사용자는 몇 대의 자동차로 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
- [x] 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4이상일 경우이다.
- [x] 자동차의 상태를 화면에 출력한다. 어느 시점에 출력할 것인지에 대한 제약은 없다.

## 프로그래밍 요구사항
- [x] 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
- 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
- UI 로직을 race.view.InputView, ResultView와 같은 클래스를 추가해 분리한다.
- [x] 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- 이 과정의 Code Style은 [intellij idea Code Style. Java](https://www.jetbrains.com/help/idea/code-style-java.html)을 따른다.
- intellij idea Code Style. Java을 따르려면 code formatting 단축키(Windows : Ctrl + Alt + L. Mac : ⌥ (Option) + ⌘ (Command) + L.)를 사용한다.
- [x] else 예약어를 쓰지 않는다.
- 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
- else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.

## 기능 목록 및 commit 로그 요구사항
- [x] 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다.
- [x] git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.
- 참고문서: [AngularJS Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153)

---
## 기능 구현 목록
- [x] 0에서 9 사이 random 값을 생성한다.
- [x] random 값이 4이상일 경우에 전진하고, 그렇지 않으면 정지한다.
- [x] 자동차 대수와 시도할 회수를 순서대로 입력받는 안내문구를 출력하고 입력받는다.
```
자동차 대수는 몇 대 인가요?
3
시도할 회수는 몇 회 인가요?
5

```
- [x] 주어진 횟수 동안 n대의 자동차는 전진하거나 멈출 수 있으며, 횟수마다 실행 결과를 출력한다.
```
실행 결과
-
-
-

--
-
--
```

---
## 1차 코멘트
- [x] View가 상태를 가지고 있는게 맞을까요?
- [x] 객체지향 설계: Car라는 도메인이 있고 그 Car가 Position을 가지고 있고, move하는 행위도 스스로 하는거 아닐까요?
- [x] 상수를 통해 해당 코드의 의미를 나타내보시면 어떨까요?

## 2차 코멘트
- [x] String.repeat() 사용
- [x] [일급컬렉션 적용](https://jojoldu.tistory.com/412) `private final List<race.domain.Car> cars = new ArrayList<>();`
- [x] [테스트 하기 좋은 코드로 인터페이스를 통해 전략패턴](https://tecoble.techcourse.co.kr/post/2020-05-17-appropriate_method_for_test_by_interface/)
- canMove에서 랜덤값을 처리하기 떄문에 실제 테스트를 하기가 어려운 문제가 있습니다. 이유는 position이 1이 될지, 0이 될지 알수가 없기 떄문이죠.

## 3차 코멘트
- [x] position을 생성할때 0이 아닌 다른 값을 넣어 생성시 문제가 생길수도 있다!
- [ ] move를 할때마다 RandomNumberGenerator를 만들 이유가 있을까요? 아래의 함수와 차이가 무엇인가요?
- [x] 10이라는 값을 상수를 통해 의미 전달을 해볼까요?
- [x] 현재는 어떻게 보면 통합테스트만 존재한다고 볼수 있습니다. Car나 CarGroup과 같은 메서드들의 단위 테스트를 추가해보시면 어떠실까요?
- [x] 대부분 collection은 Cars라는 복수를 사용하고 있습니다.
- [x] 폴더 구조도 아키텍쳐를 이해하는데에 있어 가장 좋은 방안입니다. 지금은 하나의 폴더에 모든 파일들이 있는데 아키텍쳐에 맞게 폴더(패키지)구조를 짜보시면 어떨까요?
Empty file removed src/main/java/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package calculator.util;

import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/racingcar/application/CarRaceApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.application;

import racingcar.domain.CarRace;
import racingcar.view.InputView;
import racingcar.view.ResultView;

import java.util.List;

public class CarRaceApplication {
public static void main(String[] args) {
InputView input = new InputView();
int carCount = input.getCarCount();
int runCount = input.getRunCount();

CarRace carRace = new CarRace(carCount, runCount);
List<List<Integer>> result = carRace.run();

ResultView output = new ResultView();
output.print(result);
}
}
42 changes: 42 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package racingcar.domain;

import racingcar.util.NumberGenerator;
import racingcar.util.RandomNumberGenerator;

public class Car {
private static final int MOVE_THRESHOLD = 4;
private int position;

public Car() {
this(0);
}

public Car(int position) {
validatePositive(position);
this.position = position;
}

private void validatePositive(int position) {
if (position < 0) {
throw new IllegalArgumentException("position은 0 이상이어야 합니다. position:" + position);
}
}

public int move() {
return move(new RandomNumberGenerator());
}

public int move(NumberGenerator numberGenerator) {
if (isMovable(numberGenerator)) incrementPosition();
return position;
}

private boolean isMovable(NumberGenerator numberGenerator) {
return numberGenerator.generate() >= MOVE_THRESHOLD;
}

private void incrementPosition() {
position++;
}

}
33 changes: 33 additions & 0 deletions src/main/java/racingcar/domain/CarRace.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package racingcar.domain;

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class CarRace {
private final int runCount;
private final Cars cars;

public CarRace(int carCount, int runCount) {
this.runCount = runCount;
this.cars = new Cars(createCars(carCount));
}

public List<List<Integer>> run() {
return IntStream.range(0, runCount)
.mapToObj(i -> runOnce())
.collect(Collectors.toList());
}

private List<Car> createCars(int carCount) {
return IntStream.range(0, carCount)
.mapToObj(i -> new Car())
.collect(Collectors.toList());
}

private List<Integer> runOnce() {
return cars.move();
}

}

33 changes: 33 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package racingcar.domain;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class Cars {
private final List<Car> cars;

public Cars(List<Car> cars) {
validateNotEmpty(cars);
validateElementNotNull(cars);
this.cars = new ArrayList<>(cars);
}

private void validateNotEmpty(List<Car> cars) {
if (cars == null || cars.isEmpty())
throw new IllegalArgumentException("자동차가 없습니다.");
}

private void validateElementNotNull(List<Car> cars) {
boolean hasNullElement = cars.stream().anyMatch(Objects::isNull);
if (hasNullElement)
throw new IllegalArgumentException("자동차가 null입니다.");
}

public List<Integer> move() {
return cars.stream()
.map(Car::move)
.collect(Collectors.toList());
}
}
5 changes: 5 additions & 0 deletions src/main/java/racingcar/util/NumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.util;

public interface NumberGenerator {
int generate();
}
13 changes: 13 additions & 0 deletions src/main/java/racingcar/util/RandomNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package racingcar.util;

import java.util.Random;

public class RandomNumberGenerator implements NumberGenerator {
private static final Random random = new Random();
private static final int RANDOM_UPPER_BOUND = 10;

@Override
public int generate() {
return random.nextInt(RANDOM_UPPER_BOUND);
}
}
31 changes: 31 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package racingcar.view;

import java.util.Scanner;

public class InputView {
private final Scanner scanner = new Scanner(System.in);

public int getCarCount() {
return validatePositiveInt(scanInt("자동차 대수는 몇 대 인가요?"));
}

public int getRunCount() {
return validatePositiveInt(scanInt("시도할 회수는 몇 회 인가요?"));
}

private int scanInt(String message) {
System.out.println(message);
int value = scanner.nextInt();
scanner.nextLine();
return value;
}

private int validatePositiveInt(int value) {
if (value <= 0) {
String errorMessage = String.format("입력 값은 양수여야 합니다. value:%d", value);
throw new IllegalArgumentException(errorMessage);
}
return value;
}

}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/view/ResultView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.view;

import java.util.List;

public class ResultView {
private static final String PATTERN = "-";

public void print(List<List<Integer>> result) {
System.out.println();
System.out.println("실행 결과");

for (List<Integer> runResult : result) {
for (int carPosition : runResult) {
printPattern(carPosition);
}
System.out.println();
}
System.out.println();
}

private void printPattern(int count) {
System.out.println(PATTERN.repeat(count));
}
}
Empty file removed src/test/java/.gitkeep
Empty file.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package calculator.util;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package calculator.util;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package calculator.util;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

Expand Down
26 changes: 26 additions & 0 deletions src/test/java/racingcar/domain/CarRaceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package racingcar.domain;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import racingcar.domain.CarRace;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class CarRaceTest {

@ParameterizedTest
@CsvSource(delimiter = ',', value = {"2,2", "4,5", "1,3"})
@DisplayName("자동차 경주를 실행하면, 시도 횟수 동안의 자동차의 위치를 반환한다.")
void runCarRaceAndGetResult(int carCount, int runCount) {
CarRace carRace = new CarRace(carCount, runCount);
List<List<Integer>> result = carRace.run();

assertThat(result)
.hasSize(runCount)
.allMatch(list -> list.size() == carCount);
}

}
36 changes: 36 additions & 0 deletions src/test/java/racingcar/domain/CarTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package racingcar.domain;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import racingcar.util.NumberGenerator;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class CarTest {

@Test
@DisplayName("자동차 위치가 음수이면, 예외가 발생한다.")
void constructWithNegativePosition() {
Assertions.assertThatThrownBy(() -> new Car(-1))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
@DisplayName("random 값이 4 이상이면 전진한다.")
void moveIfUpperMovableValue() {
Car car = new Car();
NumberGenerator numberGenerator = () -> 4;
assertThat(car.move(numberGenerator)).isEqualTo(1);
}

@Test
@DisplayName("random 값이 4 미만이면 정지한다.")
void stopIfLowerMovableValue() {
Car car = new Car();
NumberGenerator numberGenerator = () -> 3;
assertThat(car.move(numberGenerator)).isZero();
}

}
Loading