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

3단계 자동차 경주 #5351

Open
wants to merge 9 commits into
base: hj-rich
Choose a base branch
from
Open
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
60 changes: 52 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@

---

# 2단계
# 2단계 - 문자열 덧셈 계산기를 통한 TDD 실습

## 문자열 덧셈 계산기를 통한 TDD 실습
<details>
<summary>요구사항 보기</summary>

### 기능 요구사항

Expand Down Expand Up @@ -89,6 +90,7 @@
### 리팩터링

```java
// as-is
public static int splitAndSum(String input) {
if (isNullOrEmpty(input)) {
return DEFAULT_VALUE_FOR_EMPTY_INPUT;
Expand All @@ -110,9 +112,51 @@ public static int splitAndSum(String input) {
- null 또는 빈 문자열 체크 -> 구분자 획득 -> split -> sum
- public으로 열린 splitAndSum 메서드에서는 위 동작 흐름만 나타날 수 있게 리팩터링 해보자

- [ ] 구분자 획득
- [ ] 커스텀 구분자 정규식에
- [ ] 일치하면 커스텀 구분자를 반환
- [ ] 일치하지 않으면 기본 구분자를 반환
- [ ] split and sum
- [ ] 전달 받은 구분자로 split, parse, sum
- [x] split
- [x] 커스텀 구분자 정규식에
- [x] 일치하면 커스텀 구분자로 split한 문자열 배열을 반환
- [x] 일치하지 않으면 기본 구분자로 split한 문자열 배열을 반환
- [x] parseInt and sum
- [x] 문자열 배열을 전달받아 Stream API로 아래 메서드를 활용하여 mapToInt한 결과를 sum하여 반환
- [x] `StringAddCalculator#parsePositiveSingleNumber`: 파싱한 결과가 음수이면 예외 반환, 0 또는 양수이면 숫자 반환
- [x] `StringAddCalculator#parseSingleNumber`: 파싱한 결과가 숫자면 반환, 아닐 경우 RuntimeException 던지기

```java
// to-be
public static int splitAndSum(final String input) {
if (isNullOrEmpty(input)) {
return DEFAULT_VALUE_FOR_EMPTY_INPUT;
}

final String[] splitInput = splitByDelimiter(input);

return parseIntAndSum(splitInput);
}
```

</details>

---

# 3단계 - 자동차 경주

## 기능 요구사항

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

## 힌트

- 값을 입력 받는 API는 Scanner를 이용한다.
- 랜덤 값은 자바 java.util.Random 클래스의 nextInt(10) 메소드를 활용한다.

## 요구사항 분리

- [x] `자동차 대수는 몇 대 인가요?` -> 양의 정수 하나를 입력 받음
- [x] `시도할 회수는 몇 회 인가요?` -> 양의 정수 하나를 입력 받음
- [x] 개행 및 `실행 결과` 출력
- [x] 0-9사이 Random 값이 4이상인 경우 전진하는 메서드 구현
- [x] 입력 받은 시도 횟수 만큼 개행으로 구분하며 각 자동차의 경과를 출력
Comment on lines +156 to +162
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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

import racingcar.application.RacingCarGame;
import racingcar.domain.CarMoveGenerator;
import racingcar.domain.SixtyPercentAdvanceGenerator;
import racingcar.presentation.ConsoleRacingCarClient;
import racingcar.presentation.RacingCarClient;

public class Main {
public static void main(String[] args) {
final RacingCarClient racingCarClient = new ConsoleRacingCarClient();
final CarMoveGenerator randomCarMoveGenerator = new SixtyPercentAdvanceGenerator();

final RacingCarGame racingCarGame = new RacingCarGame(racingCarClient, randomCarMoveGenerator);
racingCarGame.play();
}
Comment on lines +10 to +16
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view 를 잘 분리해서 구현해주셨네요 👍

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

import racingcar.domain.Car;
import racingcar.domain.CarMoveGenerator;
import racingcar.domain.Cars;
import racingcar.domain.dto.RacingCarRequest;
import racingcar.domain.dto.RacingCarResult;
import racingcar.domain.vo.NumberOfCars;
import racingcar.domain.vo.NumberOfTrials;
import racingcar.presentation.RacingCarClient;

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

public class RacingCarGame {
private final RacingCarClient racingCarClient;
private final CarMoveGenerator carMoveGenerator;

public RacingCarGame(final RacingCarClient racingCarClient, final CarMoveGenerator carMoveGenerator) {
this.racingCarClient = racingCarClient;
this.carMoveGenerator = carMoveGenerator;
}

public void play() {
final RacingCarRequest racingCarInput = racingCarClient.getRacingCarInput();
final Cars cars = createCars(racingCarInput);

final NumberOfTrials numberOfTrials = racingCarInput.getNumberOfTrials();

racingCarClient.showResultHeader();
playRounds(numberOfTrials, cars);
}
Comment on lines +25 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자동차 경주의 핵심기능인 play 에 대한 테스트가 없네요..! 🤔
전략패턴으로 랜덤을 분리하였고, 입출력을 담당하는 view를 분리하였으니 테스트는 쉽게 가능할것 같아요!


private Cars createCars(final RacingCarRequest racingCarRequest) {
final NumberOfCars numberOfCars = racingCarRequest.getNumberOfCars();
final List<Car> car = createCar(numberOfCars.getValue());

return new Cars(car);
}

private List<Car> createCar(final int value) {
return IntStream.rangeClosed(1, value)
.mapToObj(i -> new Car())
.collect(Collectors.toList());
}

private void playRounds(final NumberOfTrials numberOfTrials, final Cars cars) {
for (int i = 0; i < numberOfTrials.getValue(); i++) {
cars.tryAdvance(this.carMoveGenerator);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전략패턴을 통해 잘 구현해주셨네요 👏

지금은 RacingGame 전체가 하나의 전략으로 동작하게 구현하셨는데, 각각의 Car가 다른 전략을 구현해서 사용할수도 있을것 같아요!

요구사항중 어떤차량은 30% 확률로 전진하고, 어떤 차량은 90% 확률로 전진한다면 이런 구조가 더 알맞을 수도 있을것 같아요!

그리고 외부에서 전진하는 차량만 만들어서 RacingGame 에 전달한다면, Play에 대한 테스트도 수월할지도 모르곘어요 🤔

final RacingCarResult racingCarResult = new RacingCarResult(cars.getCars());
racingCarClient.showRacingCarResult(racingCarResult);
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package racingcar.domain;

public class Car {
private int position = 1;

public void advance(final boolean advance) {
if (advance) {
this.position++;
}
}

public int getPosition() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체지향 생활 체조 원칙
규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.

게터를 사용하셨군요!!
코드를 살펴보니 테스트나 출력을 위해 사용하셨더라구요.
이런경우 트레이드오프영역으로 보고 저도 사용하곤 합니다 ㅎㅎ
다만, 메서드 이름을 좀 다르게 작성합니다.

getPositionForPrint() 와 같이 변경해서 사용합니다 :)

지금은 현재 위치를 리턴하는거니 currentPosition() 도 괜찮아 보이네요

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

@FunctionalInterface
public interface CarMoveGenerator {
boolean advance();
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.domain;

import java.util.ArrayList;
import java.util.List;

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

public Cars(final List<Car> cars) {
if (cars == null || cars.isEmpty()) {
throw new RuntimeException("자동차 목록이 비어있습니다");
}

this.cars = new ArrayList<>(cars);
}

public void tryAdvance(final CarMoveGenerator carMoveGenerator) {
cars.forEach(car -> car.advance(carMoveGenerator.advance()));
}

public List<Car> getCars() {
return cars;
}
Comment on lines +21 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급컬렉션으로 작성해주셨네요!
이럴경우 잘아시곘지만, 수정할수없는 list를 리턴하거나, 방어적복사를 통해 컬렉션을 분리해서 사용하는 방법이 있을것 같아요

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

import java.util.Random;

public class SixtyPercentAdvanceGenerator implements CarMoveGenerator {
private static final int UPPER_BOUND_EXCLUSIVE = 10;
private static final int ADVANCE_STANDARD = 4;
private final Random random = new Random();

@Override
public boolean advance() {
return random.nextInt(UPPER_BOUND_EXCLUSIVE) >= ADVANCE_STANDARD;
}
}
22 changes: 22 additions & 0 deletions src/main/java/racingcar/domain/dto/RacingCarRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package racingcar.domain.dto;

import racingcar.domain.vo.NumberOfCars;
import racingcar.domain.vo.NumberOfTrials;

public class RacingCarRequest {
private final NumberOfCars numberOfCars;
private final NumberOfTrials numberOfTrials;
Comment on lines +7 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원시값 포장 👍


public RacingCarRequest(final NumberOfCars numberOfCars, final NumberOfTrials numberOfTrials) {
this.numberOfCars = numberOfCars;
this.numberOfTrials = numberOfTrials;
}

public NumberOfCars getNumberOfCars() {
return numberOfCars;
}

public NumberOfTrials getNumberOfTrials() {
return numberOfTrials;
}
}
17 changes: 17 additions & 0 deletions src/main/java/racingcar/domain/dto/RacingCarResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package racingcar.domain.dto;

import racingcar.domain.Car;

import java.util.List;

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

public RacingCarResult(final List<Car> cars) {
this.cars = cars;
}

public List<Car> getCars() {
return cars;
}
}
21 changes: 21 additions & 0 deletions src/main/java/racingcar/domain/vo/NumberOfCars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.domain.vo;

public class NumberOfCars {
private final int value;

private NumberOfCars(final int value) {
this.value = value;
}

public static NumberOfCars from(final int numberOfCar) {
if (numberOfCar < 1) {
throw new RuntimeException("자동차는 1대 이상이어야 합니다");
}

return new NumberOfCars(numberOfCar);
}

public int getValue() {
return value;
}
}
21 changes: 21 additions & 0 deletions src/main/java/racingcar/domain/vo/NumberOfTrials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package racingcar.domain.vo;

public class NumberOfTrials {
private final int value;

private NumberOfTrials(final int value) {
this.value = value;
}

public static NumberOfTrials from(final int numberOfTrial) {
if (numberOfTrial < 1) {
throw new RuntimeException("시도 횟수는 1회 이상이어야 합니다");
}

return new NumberOfTrials(numberOfTrial);
}

public int getValue() {
return value;
}
}
65 changes: 65 additions & 0 deletions src/main/java/racingcar/presentation/ConsoleRacingCarClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package racingcar.presentation;

import racingcar.domain.Car;
import racingcar.domain.dto.RacingCarRequest;
import racingcar.domain.dto.RacingCarResult;
import racingcar.domain.vo.NumberOfCars;
import racingcar.domain.vo.NumberOfTrials;

import java.util.List;
import java.util.Scanner;

public class ConsoleRacingCarClient implements RacingCarClient {
private static final String LINE_SEPARATOR = System.lineSeparator();
private static final String PROGRESS_CHARACTER = "-";
private static final String MESSAGE_FOR_CAR_INPUT_REQUEST = "자동차 대수는 몇 대 인가요?";
private static final String MESSAGE_FOR_TRIAL_INPUT_REQUEST = "시도할 회수는 몇 회 인가요?";
private static final String MESSAGE_FOR_INVALID_CAR_INPUT = "자동차 대수를 양의 숫자로 입력해주세요 : %s";
private static final String MESSAGE_FOR_INVALID_TRIAL_INPUT = "시도 횟수를 양의 숫자로 입력해주세요 : %s";
private static final String RESULT_HEADER = "%s실행 결과%s";
private final Scanner scanner = new Scanner(System.in);

@Override
public RacingCarRequest getRacingCarInput() {
final NumberOfCars numberOfCars = getNumberOfCars();
final NumberOfTrials numberOfTrials = getNumberOfTrials();
scanner.close();

return new RacingCarRequest(numberOfCars, numberOfTrials);
}

private NumberOfCars getNumberOfCars() {
System.out.println(MESSAGE_FOR_CAR_INPUT_REQUEST);
final String input = scanner.nextLine();
try {
final int parsed = Integer.parseInt(input);
return NumberOfCars.from(parsed);
} catch (NumberFormatException e) {
throw new RuntimeException(String.format(MESSAGE_FOR_INVALID_CAR_INPUT, input), e);
}
}

private NumberOfTrials getNumberOfTrials() {
System.out.println(MESSAGE_FOR_TRIAL_INPUT_REQUEST);
final String input = scanner.nextLine();
try {
final int parsed = Integer.parseInt(input);
return NumberOfTrials.from(parsed);
} catch (NumberFormatException e) {
throw new RuntimeException(String.format(MESSAGE_FOR_INVALID_TRIAL_INPUT, input), e);
}
}

@Override
public void showResultHeader() {
System.out.printf(RESULT_HEADER, LINE_SEPARATOR, LINE_SEPARATOR);
}

@Override
public void showRacingCarResult(final RacingCarResult racingCarResult) {
final List<Car> cars = racingCarResult.getCars();
cars.forEach(car -> System.out.println(PROGRESS_CHARACTER.repeat(car.getPosition())));

System.out.println();
}
}
12 changes: 12 additions & 0 deletions src/main/java/racingcar/presentation/RacingCarClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package racingcar.presentation;

import racingcar.domain.dto.RacingCarRequest;
import racingcar.domain.dto.RacingCarResult;

public interface RacingCarClient {
RacingCarRequest getRacingCarInput();

void showResultHeader();

void showRacingCarResult(RacingCarResult result);
}
Loading