Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
eb38c39
docs: 구현 기능 목록 작성
masonkimseoul Feb 14, 2024
9b41a8d
feat: 자동차 Domain Class 생성
masonkimseoul Feb 14, 2024
3bb9da8
feat: 자동차 목록을 저장하는 Domain class 생성
jongmee Feb 14, 2024
96ed9f7
feat: 랜덤 숫자 생성 Domain class 생성
jongmee Feb 14, 2024
bc87041
feat: 각 시도 실행 결과 출력하는 메서드 생성
masonkimseoul Feb 14, 2024
5641bfa
feat: 우승자 출력 및 안내 메세지 출력 메서드 생성
jongmee Feb 14, 2024
be081af
feat: 문자열 입력 및 파싱 메서드 생성
masonkimseoul Feb 14, 2024
5cd7dcf
feat: 자동차 이름 길이 예외 처리 메서드 생성
masonkimseoul Feb 14, 2024
8770488
feat: 중복된 자동차 이름 검증 메서드 생성
jongmee Feb 14, 2024
4f55e91
refactor: 자동차 이름 파싱 메서드 utility 패키지로 분리
jongmee Feb 14, 2024
02fd68a
fix: 자동차 이름 목록 검증 메서드 수정
jongmee Feb 14, 2024
66c07da
feat: 시도 회수 검증 메서드 생성
masonkimseoul Feb 14, 2024
c25f173
refactor: Validation 클래스 이름 변경
masonkimseoul Feb 14, 2024
fa8cf97
feat: 컨트롤러 의존성 주입 설정
masonkimseoul Feb 14, 2024
c12059b
feat: Cars 도메인 클래스에서 자동차들을 움직이는 메서드 구현
jongmee Feb 15, 2024
9b1eea8
feat: controller에서 입력 및 자동차 이동 기능 구현
jongmee Feb 15, 2024
93d4827
feat: 승리자 결정 메서드 생성
masonkimseoul Feb 15, 2024
5038390
feat: 우승자를 정하는 기능 구현
jongmee Feb 15, 2024
60f3877
feat: main 메서드 생성
jongmee Feb 15, 2024
c6abad0
refactor: controller에서 자동차 생성 기능 분리
masonkimseoul Feb 15, 2024
044b15a
refactor: outputview에서 실행 결과 출력 방식 변경
masonkimseoul Feb 15, 2024
acedb4e
refactor: Cars 생성자 리팩토링, controller에서 시도횟수 입력 기능 분리
jongmee Feb 15, 2024
bb6efbe
fix: 자동차 이름 입력시 양옆 공백 제거
jongmee Feb 15, 2024
12b2de5
test: Car 클래스 테스트 작성
masonkimseoul Feb 15, 2024
b0085b2
test: Validator 클래스 테스트 작성
jongmee Feb 15, 2024
e368b06
test: Parser 클래스 테스트 작성
jongmee Feb 15, 2024
c9b5df6
refactor: RandomNumber 클래스를 인터페이스로 변경
masonkimseoul Feb 15, 2024
3afe80f
test: Cars 클래스 테스트 작성
jongmee Feb 15, 2024
4627def
test: Cars에서 다수의 우승자 선정 테스트 작성
masonkimseoul Feb 15, 2024
8ae2b44
chore: 코드 스타일 정리
masonkimseoul Feb 15, 2024
b41c3b4
refactor: Config에서 Manager로 클래스명 변경, controller run메서드명 변경
jongmee Feb 16, 2024
0abac6e
refactor: controller 메서드 순서 변경
jongmee Feb 16, 2024
94a373a
refactor: Car 클래스 내 의미가 모호한 변수명 변경
jongmee Feb 16, 2024
e889b38
refactor: RandomNumber 도메인 클래스명 RandomNumberGenerator로 변경
jongmee Feb 16, 2024
0803b83
fix: 소스파일마다 개행 추가
jongmee Feb 16, 2024
fd77b3d
refactor: Cars 도메인 클래스 메서드명 변경
jongmee Feb 16, 2024
e49082a
docs: README 불필요한 부분 삭제
jongmee Feb 16, 2024
13d1393
refactor: RandomNumberGenerator를 경주 당 하나만 생성하도록 변경
jongmee Feb 16, 2024
2f6c7ef
refactor: 역할 설명이 필요한 상수를 static final로 관리하기
jongmee Feb 16, 2024
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
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
# java-racingcar
# [자동차 경주] 구현 기능 목록

자동차 경주 미션 저장소
## 핵심 구현 기능

## 우아한테크코스 코드리뷰
1. 이름을 가진 N대의 자동차를 생성한다.
2. 사용자가 입력한 횟수만큼 각 자동차는 전진 또는 정지할 수 있다.
3. 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
4. 한 명 이상의 우승자를 구한다.
1. 모든 자동차의 전진 횟수가 0회인 경우도 우승으로 간주한다.

- [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md)
## 입출력 기능

### 입력

1. 자동차 이름들을 담은 문자열을 입력한다.
1. 각 자동차 이름은 다섯 글자 이하로 한다.
2. 각 자동차 이름은 쉼표로 구분된다.
2. 시도할 회수를 입력한다.

### 출력

1. 각 시도 마다 실행 결과를 출력한다.
1. 이름의 순서는 입력된 순서로 출력한다.
2. 1회 전진 시 ‘-’ 1개를 출력한다.
3. 1회 정지 시 아무것도 출력하지 않는다.
2. 우승자 이름을 출력한다.
1. 이름의 순서는 입력된 순서로 출력한다.
2. 각 이름은 쉼표로 구분한다.

## 예외 처리 기능

1. 자동차 이름이 1자 미만 5자 초과인 경우 예외로 처리한다.
2. 중복된 자동차 이름이 입력된 경우 예외로 처리한다.
3. 아예 빈 문자열 혹은 null이 입력된 경우 예외로 처리한다.
4. 시도할 회수에 정수가 아닌 문자열이 입력된 경우 예외로 처리한다.
5. 시도할 회수에 0 또는 양이 아닌 정수가 입력된 경우 예외로 처리한다.
10 changes: 10 additions & 0 deletions src/main/java/racingcar/Application.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package racingcar;

import racingcar.manager.RacingManager;

public class Application {
public static void main(String[] args) {
RacingManager manager = new RacingManager();
manager.racingController().start();
}
}
68 changes: 68 additions & 0 deletions src/main/java/racingcar/controller/RacingController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package racingcar.controller;

import racingcar.domain.Car;
import racingcar.domain.Cars;
import racingcar.domain.RandomNumberGenerator;
import racingcar.domain.RandomNumberGeneratorImpl;
import racingcar.util.Parser;
import racingcar.util.Validator;
import racingcar.view.InputView;
import racingcar.view.OutputView;

import java.util.List;

public class RacingController {
private final InputView inputView;
private final OutputView outputView;

public RacingController(final InputView inputView, final OutputView outputView) {

Choose a reason for hiding this comment

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

각종 곳에 final이 붙어있는데, final을 붙였을 때 어떤 장/단점이 있을지
그에 따라 어디에 final을 붙이는 것이 가장 좋을지 고민해보는 것도 좋습니다.

이 부분은 취향이 갈리는 부분이라 크루들과 이야기를 나눠보는 것도 좋을 것 같아요.
미아만의 기준을 찾아보길 바랍니다.

this.inputView = inputView;
this.outputView = outputView;
}

public void start() {
final List<String> carNames = readCarNames();
final Cars cars = new Cars(carNames);
final int tryCount = readTryCount();

outputView.printResultMsg();
RandomNumberGenerator randomNumberGenerator = new RandomNumberGeneratorImpl();
for(int i = 0 ; i < tryCount; i++) {
moveCars(cars, randomNumberGenerator);
}

final List<Car> winners = cars.determineWinner();
outputView.printWinners(winners);
inputView.closeScanner();
}

private List<String> readCarNames() {
try {
String carNames = inputView.readCarNames();
Validator.validateNullName(carNames);
List<String> parsedCarNames = Parser.parseCarNames(carNames);
Validator.validateCarNames(parsedCarNames);
Comment on lines +42 to +44

Choose a reason for hiding this comment

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

이름에 대한 고민들을 해볼까요?

Validator나 Parser의 클래스명을 보았을 때, 해당 클래스는 어떤 일을 하는 것으로 유추할 수 있을까요?
또한 ValidateNullName과 validateCarNames이라는 메서드명만 보았을 때, 이 메서드는 어떤 일을 하는 것으로 유추를 할 수 있을까요?

이 부분에 대해서 고민하다보면 Validator.class를 util 패키지 내에 위치시켜서 유효성 검증 관련 로직을 모아뒀는데(추후 RacingController.class에서 validation 메서드를 호출함), view 혹은 domain에서 유효성 검증을 진행하는게 더 좋을까요? 질문에 대한 미아만의 답변이 될 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

유효성 검증(Validator)과 분석(Parser)하는 클래스로 유추될 거 같아요.
validateNullName 메서드는 null로 입력된 무언가의 이름을, validateCarNames 메서드는 자동차의 이름을 검증하는 메서드로 유추할 것 같습니다.

정확하고 구체적으로 하는 일을 유추하기 어렵네요.. 검증하고 분석하는 대상을 쉽게 찾을 수 있으려면, view나 domain으로 메서드 위치를 옮기는게 좋을 거 같네요!

Choose a reason for hiding this comment

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

예를 들어 CarNames의 검증 역할이 Controller에 있다면, Cars라는 도메인은 자신의 이름을 검증할 책임이 없게 됩니다.
즉, 다른 사람들이 해당 Cars를 사용할 때는, validate가 되지 않은 이름을 사용해도 무관하게 되는거죠!

반대로 도메인에 있다면, Cars라는 도메인은 자신의 이름을 검증할 책임이 있게 됩니다.
즉, 다른 사람들은 해당 Cars를 사용할 때, 항상 validate가 된 이름을 사용해야하는 의무가 생기는거죠!

이렇게 어디다가 두는 것이 옳다!가 아니라 미아의 의도에 맞춰서 도메인들을 설계해주는 것이 중요합니다.
해당 프로그램에서 과연 이 도메인들은 자신이 어떤 책임들을 가지고 있는 게 좋을지 고민해보는 것도 좋을 것 같네요 😃

(이 부분은 코드를 짜면서 익히는 것도 좋지만, 1레벨 필독서들을 읽으면 또 다른 느낌으로 와닿을 것이라 생각해요!!
심심할 때 틈틈히 읽어놓으면 좋습니다.)

return parsedCarNames;
} catch (IllegalArgumentException e) {
outputView.printErrorMsg(e.getMessage());
return readCarNames();
}
}

private int readTryCount() {
try {
String tryCount = inputView.readTryCount();
int parsedTryCount = Validator.validateInteger(tryCount);
Validator.validateTryCount(parsedTryCount);
return parsedTryCount;
} catch (IllegalArgumentException e) {
outputView.printErrorMsg(e.getMessage());
return readTryCount();
}
}

private void moveCars(final Cars cars, final RandomNumberGenerator randomNumberGenerator) {
cars.moveAll(randomNumberGenerator);
outputView.printCarPosition(cars);
}
}
35 changes: 35 additions & 0 deletions src/main/java/racingcar/domain/Car.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package racingcar.domain;

public class Car {
private static final int MOVEMENT_CRITERIA = 3;

private final String name;
private int movement;

public Car(final String name) {
this.name = name;
this.movement = 0;
}

public void move(final int condition) {
if (condition > MOVEMENT_CRITERIA ) {
this.movement += 1;
}
}

public boolean isSameCount(final int count) {

Choose a reason for hiding this comment

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

movement로 바뀌었지만 요건 count로 남아있네요 😃

return this.movement == count;
}

public boolean isAlsoWinner(final Car car) {
return car.isSameCount(movement);
}

public String getName() {
return name;
}

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

import java.util.Comparator;
import java.util.List;

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

public Cars(final List<String> names) {
this.cars = names.stream().map(Car::new).toList();
}

public void moveAll(final RandomNumberGenerator randomNumberGenerator) {
for(Car car: cars) {
final int condition = randomNumberGenerator.generate();
car.move(condition);
}
}

public List<Car> getCars() {
return this.cars;
}

public List<Car> determineWinner() {
final Car winnerCar = cars.stream()
.max(Comparator.comparing(Car::getMovement))
.get();
return cars.stream()
.filter(car -> car.isAlsoWinner(winnerCar))
.toList();
}
}
5 changes: 5 additions & 0 deletions src/main/java/racingcar/domain/RandomNumberGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package racingcar.domain;

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

import java.util.Random;

public class RandomNumberGeneratorImpl implements RandomNumberGenerator {
public int generate() {
Random random = new Random();
return random.nextInt(10);
}
}
19 changes: 19 additions & 0 deletions src/main/java/racingcar/manager/RacingManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package racingcar.manager;

import racingcar.controller.RacingController;
import racingcar.view.InputView;
import racingcar.view.OutputView;

public class RacingManager {
private InputView inputView() {

Choose a reason for hiding this comment

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

메서드명 명명 규칙을 고민해보면, 명사로 선언을 해도 괜찮을까요?
Spring이나 다른 프레임워크의 기반으로 생각하지 않고 Java 프로그램만 생각해서 명명규칙들을 맞춰줘보면 좋을 것 같아요 😃

return new InputView();
}

private OutputView outputView() {
return new OutputView();
}

public RacingController racingController() {
return new RacingController(inputView(), outputView());
}
}
12 changes: 12 additions & 0 deletions src/main/java/racingcar/util/Parser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package racingcar.util;

import java.util.Arrays;
import java.util.List;

public class Parser {
public static List<String> parseCarNames(final String input) {
return Arrays.stream(input.split(","))
.map(String::trim)
.toList();
}
}
46 changes: 46 additions & 0 deletions src/main/java/racingcar/util/Validator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package racingcar.util;

import java.util.List;
import java.util.Set;

public class Validator {
public static void validateNullName(final String carNames) {
if(carNames == null) {
throw new IllegalArgumentException("자동차 이름 목록은 null일 수 없습니다.");
}
}

public static void validateCarNames(final List<String> carNames) {
for(String carName: carNames) {
validateCarNamesFormat(carName);
}
validateDuplicatedNames(carNames);
}

private static void validateCarNamesFormat(final String carName) {
if (carName.isBlank() || carName.isEmpty() || carName.length() > 5) {
throw new IllegalArgumentException("자동차 이름은 1자 이상 5자 이하여야 합니다.");
}
}

private static void validateDuplicatedNames(final List<String> carNames) {
final Set<String> uniqueNames = Set.copyOf(carNames);
if(uniqueNames.size() != carNames.size()) {
throw new IllegalArgumentException("자동차 이름은 중복될 수 없습니다.");
}
}

public static int validateInteger(final String tryCount) {
try {
return Integer.parseInt(tryCount);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("시도 회수는 정수여야 합니다.");
}
}

public static void validateTryCount(final int tryCount) {
if (tryCount < 1) {
throw new IllegalArgumentException("시도할 회수는 양의 정수여야 합니다.");
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/racingcar/view/InputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package racingcar.view;

import java.util.Scanner;

public class InputView {
private static final String CAR_NAMES_INPUT_MSG = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).";
private static final String TRY_COUNT_INPUT_MSG = "시도할 회수는 몇회인가요?";
Comment on lines +6 to +7

Choose a reason for hiding this comment

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

코드에 어떤 부분은 상수로 문자열을 표시하고, 어떤 부분은 직접 문자열을 표시하고 있네요.
미아는 어떤 기준으로 상수를 따로 static으로 선언하였나요?

Copy link
Author

Choose a reason for hiding this comment

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

역할 설명이 필요하다고 생각하는 문자열은 static final 으로 선언하고자 했는데, 추가로 필요한 부분이 있는지 다시 살펴보겠습니다!


private final Scanner scanner = new Scanner(System.in);

public String readCarNames() {
System.out.println(CAR_NAMES_INPUT_MSG);
return scanner.nextLine();
}

public String readTryCount() {
System.out.println(TRY_COUNT_INPUT_MSG);
return scanner.nextLine();
}

public void closeScanner() {
scanner.close();
}
}
39 changes: 39 additions & 0 deletions src/main/java/racingcar/view/OutputView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package racingcar.view;

import java.util.List;
import racingcar.domain.Car;
import racingcar.domain.Cars;

public class OutputView {
private static final String RESULT_MSG = "실행 결과";
private static final String POSITION_FORM = "%s : %s";
private static final String TRACE = "-";
private static final String WINNER_MSG = "%s가 최종 우승했습니다.";

public void printCarPosition(final Cars cars) {
for(Car car : cars.getCars()) {
final String name = car.getName();
final String traces = makeTraces(car.getMovement());
System.out.println(String.format(POSITION_FORM, name, traces));
}
System.out.println();
}

private String makeTraces(final int count) {
return TRACE.repeat(count);
}

public void printWinners(final List<Car> winners) {
List<String> winnerNames = winners.stream().map(Car::getName).toList();
final String names = String.join(", ", winnerNames);
System.out.println(String.format(WINNER_MSG, names));
}

public void printResultMsg() {
System.out.println(RESULT_MSG);
}

public void printErrorMsg(String errorMsg) {
System.out.println(errorMsg);
}
}
Loading