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

[자동차 경주 게임] 박진훈 리뷰 부탁드립니다! #522

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
0556a70
docs: 추가 기능목록
invalid-email-address Dec 6, 2022
8f1220e
feat: 추가 skeleton
invalid-email-address Dec 6, 2022
7935b9f
fix: 변경 자바버전 설정
invalid-email-address Dec 6, 2022
db4cb02
feat: 추가 자동차에 이름을 입력받는 기능
invalid-email-address Dec 6, 2022
ca3eda7
test: 추가 자동차에 이름을 입력받는 기능테스트
invalid-email-address Dec 6, 2022
c117b2b
feat: 추가 주어진 횟수를 입력받는 기능
invalid-email-address Dec 6, 2022
134df50
test: 추가 주어진 횟수를 입력받는 기능테스트
invalid-email-address Dec 6, 2022
3fc2bae
feat: 추가 자동차는 전진 또는 멈춤 기능
invalid-email-address Dec 6, 2022
9f8abc4
test: 추가 자동차는 전진 또는 멈춤 기능 테스트
invalid-email-address Dec 6, 2022
76ad6d5
feat: 추가 주어진 횟수 동안 n대의 자동차는 전진 또는 멈춤 기능
invalid-email-address Dec 6, 2022
c3f9689
test: 추가 주어진 횟수 동안 n대의 자동차는 전진 또는 멈춤 테스트기능
invalid-email-address Dec 6, 2022
d001728
feat: 추가 자동차 경주 게임을 완료한 후 누가 우승했는지 출력하는 기능
invalid-email-address Dec 6, 2022
6cc73d5
test: 추가 자동차 경주 게임을 완료한 후 누가 우승했는지 출력하는 기능테스트
invalid-email-address Dec 6, 2022
5a0b432
fix: 변경 예외처리 메세지 출력방식
invalid-email-address Dec 6, 2022
e5e4263
refactor: 수정 Cars 객체생성
invalid-email-address Dec 6, 2022
1f07840
refactor: 수정 outputView
invalid-email-address Dec 6, 2022
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ dependencies {

java {
toolchain {
languageVersion = JavaLanguageVersion.of(8)
languageVersion = JavaLanguageVersion.of(11)
}
}

Expand Down
20 changes: 20 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# 자동차 경주 게임

## 기능 목록

### 자동차 게임 기능
- [x] 주어진 횟수를 입력받는 기능
- [x] 횟수가 숫자가 아닐시 예외 발생 기능
- [x] 횟수가 음수 또는 너무 큰 수 일시 예외 발생 기능
- [x] 주어진 횟수 동안 n대의 자동차는 전진 또는 멈춤 기능
- [x] 움직인 결과 출력 기능
- [x] 자동차 경주 게임을 완료한 후 누가 우승했는지 출력하는 기능
- [x] 우승자가 여러 명일 경우 쉼표(,)를 이용하여 구분하여 출력하는 기능

### 자동차 기능
- [x] 자동차에 이름을 입력받는 기능
- [x] 자동차 이름은 쉼표(,)를 기준으로 구분하는 기능
- [x] 쉼표(,) 여러개를 입력해도 인식 가능한 기능
- [x] 이름은 5자 초과시 예외 발생 기능
- [x] 자동차는 전진 또는 멈춤 기능
- [x] 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 때 전진 기능
6 changes: 5 additions & 1 deletion src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package racingcar;

import racingcar.controller.RacingCarController;

public class Application {

public static void main(String[] args) {
// TODO 구현 진행
RacingCarController racingCarController = new RacingCarController();
racingCarController.runGame();
}
}
12 changes: 0 additions & 12 deletions src/main/java/racingcar/Car.java

This file was deleted.

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

import racingcar.domain.CarRandomMoveNumberGenerator;
import racingcar.domain.Cars;
import racingcar.domain.RacingCarGame;
import racingcar.domain.TryCommand;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class RacingCarController {

private final InputView inputView = new InputView();
private final OutputView outputView = new OutputView();
private RacingCarGame racingCarGame;

public void runGame() {
makeRacingCarGame();
playGame();
endGame();
}

private void endGame() {
outputView.printWinner(racingCarGame.findWinner());
}

private void playGame() {
TryCommand tryCommand = askTryCommand();
while(tryCommand.tryMove()){
racingCarGame.move();
outputView.printResult(racingCarGame.getCars());
}
}

private void makeRacingCarGame() {
racingCarGame = new RacingCarGame(askCar());
outputView.printBlank();
}

private TryCommand askTryCommand() {
outputView.printInputTry();
return new ReEnterProcessor<>(inputView::readTryCommand, outputView::printExceptionMessage).process();
}


private Cars askCar() {
outputView.printInputCarName();
return new ReEnterProcessor<>(inputView::readCarName, outputView::printExceptionMessage).process();
}
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/controller/ReEnterProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.controller;

import java.util.function.Consumer;
import java.util.function.Supplier;

public class ReEnterProcessor<T> {
private final Supplier<T> processor;
private final Consumer<IllegalArgumentException> consumer;

public ReEnterProcessor(Supplier<T> processor, Consumer<IllegalArgumentException> consumer) {
this.processor = processor;
this.consumer = consumer;
}

public T process() {
while(true){
try {
return processor.get();
}catch (IllegalArgumentException exception){
consumer.accept(exception);
}
}
}
}
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.exception.CarNameLengthException;

public class Car {

private static final int CAR_NAME_LENGTH = 5;
private static final int MOVABLE_MIN_NUMBER = 4;
private final String name;
Comment on lines +8 to +9
Copy link

Choose a reason for hiding this comment

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

컨벤션에 따라서 상수와 인스턴스 변수를 새 줄로 구분해주시는 게 좋을 것 같습니다!!

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다! 놓치고있던 부분이네요..

private int position = 0;

public Car(String name) {
validate(name);
this.name = name;
}

private void validate(String name) {
validateLength(name);
}

private void validateLength(String name) {
if (name.length() > CAR_NAME_LENGTH) {
throw new CarNameLengthException();
}
}

public void move(CarMoveNumberGenerator carMoveNumberGenerator) {
final int number = carMoveNumberGenerator.generate();

if (number >= MOVABLE_MIN_NUMBER) {
position++;
}
}

public String getName() {
return name;
}

public int getPosition() {
return position;
}
}
6 changes: 6 additions & 0 deletions src/main/java/racingcar/domain/CarMoveNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package racingcar.domain;

public interface CarMoveNumberGenerator {

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

import camp.nextstep.edu.missionutils.Randoms;

public class CarRandomMoveNumberGenerator implements CarMoveNumberGenerator {

private static final int RANDOM_LOWER_INCLUSIVE = 0;
private static final int RANDOM_UPPER_INCLUSIVE = 9;

@Override
public int generate() {
return Randoms.pickNumberInRange(RANDOM_LOWER_INCLUSIVE, RANDOM_UPPER_INCLUSIVE);
}
}
71 changes: 71 additions & 0 deletions src/main/java/racingcar/domain/Cars.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package racingcar.domain;

import racingcar.exception.CarsDuplicatedNameException;
import racingcar.exception.CarsMaxScoreBlankException;

import java.util.*;
import java.util.stream.Collectors;

public class Cars {

private static final String DELIMITER = ",";
private static final String DUPLICATED_DELIMITER_REGEX = ",+";
private final List<Car> cars;

private Cars(List<Car> cars) {
this.cars = Collections.unmodifiableList(cars);
}
Comment on lines +15 to +17
Copy link

Choose a reason for hiding this comment

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

불변 객체를 만들 때 생성자 단계에서는 방어적 복사를 고려해주시면 더 좋을 것 같아요!!

Copy link
Author

Choose a reason for hiding this comment

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

오... 좋은글 감사합니다. 방어적복사는 처음 접해보는 개념이네요!


public static Cars createCarNameByWord(String input) {
List<Car> cars = new ArrayList<>();
String[] words = divideWord(input);
validate(words);

for (String carName : words) {
cars.add(new Car(carName));
}
return new Cars(cars);
Comment on lines +24 to +27
Copy link

Choose a reason for hiding this comment

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

스트림으로 로직을 바꾸시면 빈 리스트를 선언하지 않고 더 깔끔하게 구현할 수 있을 것 같아요!

}

private static void validate(String[] words) {
validateDuplicatedWord(words);
}

private static void validateDuplicatedWord(String[] words) {
Set<String> uniqueWord = new HashSet<>();
for (String word : words) {
if (!uniqueWord.add(word)) {
throw new CarsDuplicatedNameException();
}
}
}

private static String[] divideWord(String word) {
return word.replaceAll(DUPLICATED_DELIMITER_REGEX, DELIMITER).split(DELIMITER);
}

public void move(CarMoveNumberGenerator carMoveNumberGenerator) {
for (Car car : cars) {
car.move(carMoveNumberGenerator);
Copy link

Choose a reason for hiding this comment

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

CarMoveNumberGeneratorCar 객체까지 넘겨주지 않아도 될 것 같습니다!
그러면 car.move() 메서드를 테스트하기도 훨씬 간편해질 것 같아요!

}
}

public Winner findWinner() {
int maxScore = findMaxScore();
return new Winner(cars.stream()
.filter(car -> (car.getPosition() == maxScore))
.map(Car::getName)
.collect(Collectors.toList()));
}

private int findMaxScore() {
return cars.stream()
.max(Comparator.comparingInt(Car::getPosition))
.map(Car::getPosition)
.orElseThrow(CarsMaxScoreBlankException::new);
}

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

public class RacingCarGame {
Copy link

Choose a reason for hiding this comment

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

RacingCarGame 클래스의 메서드들이 대부분 Cars 클래스의 메서드를 호출하는 용도로만 사용되는 것 같아요!
RacingCarGame 클래스가 정말 필요한지, 아니면 어떤 식으로 RacingCarGame 만의 새로운 책임을 부여할 수 있을 지
생각해보시면 좋을 것 같아요!!

Copy link
Author

Choose a reason for hiding this comment

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

원래 RacingCarGame 에서 시도횟수를 가지고있어서 Cars 를 시도횟수만큼 움직이는 로직이있었는데 controller 쪽으로 해당 로직을 옮기는바람에 쓸모없는 클래스가 되었네요.. 좋은 지적 감사합니다..!


private final Cars cars;

public RacingCarGame(Cars cars) {
this.cars = cars;
}

public void move() {
cars.move(new CarRandomMoveNumberGenerator());
}

public Winner findWinner() {
return cars.findWinner();
}

public Cars getCars() {
return cars;
}
}
47 changes: 47 additions & 0 deletions src/main/java/racingcar/domain/TryCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package racingcar.domain;

import racingcar.exception.TryCommandNumberException;
import racingcar.exception.TryCommandRangeException;

public class TryCommand {

private static final int MIN_TRY = 1;
private static final int MAX_TRY = 100000;
private int tryCount;
Copy link

Choose a reason for hiding this comment

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

값 객체의 내부 상태를 final 로 선언하는 방법도 있더라구요! 참고해보시면 좋을 것 같습니다!

Copy link
Author

Choose a reason for hiding this comment

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

불변으로 해도 값을 변경하는 방법이 있었군요..
DDD 에서 나오는 개념이라 MVC 에는 어떻게 적용할지는 고민을 해봐야겠네요..
좋은글 감사합니다!


private TryCommand(int tryCount) {
this.tryCount = tryCount;
}

public static TryCommand createTryCommandByString(String input) {
int number = convertInt(input);
validate(number);
return new TryCommand(number);
}

private static void validate(int number) {
validateRange(number);
}

private static void validateRange(int number) {
if(number < MIN_TRY || number > MAX_TRY) {
throw new TryCommandRangeException(MIN_TRY, MAX_TRY);
}
}

private static int convertInt(String input) {
try{
return Integer.parseInt(input);
}catch (NumberFormatException exception) {
throw new TryCommandNumberException();
}
}

public boolean tryMove() {
if(tryCount > 0) {
tryCount--;
return true;
}
return false;
}
}
16 changes: 16 additions & 0 deletions src/main/java/racingcar/domain/Winner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package racingcar.domain;

import java.util.List;

public class Winner {

private final List<String> winner;
Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

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

리스트의 네이밍은 복수형이 더 적절하다고 생각합니다!
Winner 클래스 자체도 승리자들의 이름을 담고있는 일급 컬렉션이기 때문에 Winners 라는 네이밍을 더 추천드립니다!


public Winner(List<String> winner) {
this.winner = winner;
}

public List<String> getWinner() {
return winner;
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/exception/CarNameLengthException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.exception;

public class CarNameLengthException extends IllegalArgumentException{
Copy link

Choose a reason for hiding this comment

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

특별히 커스텀 예외를 사용하신 이유가 있을까요?? 궁금합니다!

Copy link
Author

Choose a reason for hiding this comment

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

커스텀예외

커스텀 예외들은 장단점이 있습니다! 그래서 정답은 없지만 제가 사용한 이유는
첫째로클래스 이름으로 어떤 예외인지 바로 알 수 있고
둘째로 보통 도메인 코드를 볼때 중요한것은 그 도메인 역할과 관련된 로직이지, 예외처리를 어떻게 하는지가 중요한게 아니기때문에 예외를 따로빼내어 좀 더 보기 편한게 만들기 위함입니다!
셋째로 대부분 Spring MVC 프로젝트에서는 커스텀 예외를 사용하기에 연습차원에서 커스텀 예외를 사용하고 있습니다.


private static final String EXCEPTION_MESSAGE_CAR_NAME_LENGTH = "[ERROR] 자동차이름은 5글자 이하입니다";

public CarNameLengthException() {
super(EXCEPTION_MESSAGE_CAR_NAME_LENGTH);
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/exception/CarsDuplicatedNameException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.exception;

public class CarsDuplicatedNameException extends IllegalArgumentException {

private static final String EXCEPTION_MESSAGE_CARS_DUPLICATED_NAME = "[ERROR] 자동차 이름은 중복될 수 없습니다";

public CarsDuplicatedNameException() {
super(EXCEPTION_MESSAGE_CARS_DUPLICATED_NAME);
}
}
10 changes: 10 additions & 0 deletions src/main/java/racingcar/exception/CarsMaxScoreBlankException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar.exception;

public class CarsMaxScoreBlankException extends IllegalArgumentException{

private static final String EXCEPTION_MESSAGE_CARS_MAX_SCORE_BLANK = "[ERROR] 우승자를 찾을 수 없습니다.";

public CarsMaxScoreBlankException() {
super(EXCEPTION_MESSAGE_CARS_MAX_SCORE_BLANK);
}
}
Loading