-
Notifications
You must be signed in to change notification settings - Fork 93
[자동차 경주] 이상현 미션 제출합니다. #120
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
Changes from 29 commits
679d6a3
a81fdd9
2c5a032
f29d936
a70fdba
74bdd3a
8668af9
f2530c6
b1b5dbf
4d38a71
726078e
5bad0a3
c7b2261
7e71b3c
6e4819f
660f552
a60dd48
eaecd52
88dcf42
e2ea68b
9f3e933
1c62c88
4dba7c5
26fa5d5
6494866
e9887c8
a9dccde
5409d06
56c62d3
b4c3bfd
460375e
eb9ee56
f76307b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# 자동차 경주 - 초간단 애플리케이션 | ||
|
||
## 1단계 요구사항 | ||
- 자동차는 이름과 거리를 가지고 있다. | ||
- 자동차는 움직일 수 있다. | ||
- 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다. | ||
- 0 ~ 9 사이의 random 한 값을 생성시키는 객체가 필요하다. | ||
|
||
## 2단계 요구사항 | ||
- n대의 자동차가 참여할 수 있다. | ||
- 자동차 경주 게임을 완료한 후 누가 우승했는지를 구할 수 있다. 우승자는 한 명 이상일 수 있다. | ||
- 경주 게임이라는 객체가 필요해보인다. | ||
- 생성자로 n대의 자동차 객체가 들어간다 | ||
- 게임을 진행하는 메소드가 필요해보인다. | ||
- 주어진 횟수가 끝나면 자동차끼리의 distance를 비교하여 자동차 이름 리스트를 반환하고 저장한다. | ||
|
||
## 3단계 요구사항 | ||
- 메인 메서드를 추가하여 실행 가능한 애플리케이션으로 만든다. | ||
- 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다. | ||
- 출력문에 대한 문자열 관리 | ||
- 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다. | ||
- 자동차 생성시 이름 길이에 대한 유효성 검증 | ||
- 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다. | ||
- 이동 횟수가 정수인지 유효성 검증 | ||
|
||
## 4단계 요구사항 정리 | ||
- 게임 시도 횟수 입력 시 정수가 아니면 예외 처리에 대한 문구 추가하기 | ||
- 모든 로직에 단위 테스트를 구현 | ||
- 예외 사항 더 추상화하여 처리해보기 | ||
- 자동차 이름 비었을 때 | ||
- 자동차 이름 6자 이상일 때 | ||
|
||
## 프로그래밍 요구사항 | ||
|
||
- 자동차가 움직이는 기능이 의도대로 동작하는지 테스트한다. | ||
- 자바 코드 컨벤션을 지키면서 프로그래밍한다. | ||
- 기본적으로 Java Style Guide을 원칙으로 한다. | ||
- indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다. | ||
- 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다. | ||
- 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메서드)를 분리하면 된다. | ||
- 3항 연산자를 쓰지 않는다. | ||
- else 예약어를 쓰지 않는다. | ||
- else 예약어를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다. | ||
- 힌트: if문에서 값을 반환하는 방식으로 구현하면 else 예약어를 사용하지 않아도 된다. | ||
- 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다. | ||
- 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import domain.Car; | ||
import domain.CarNameParser; | ||
import domain.CarRace; | ||
import domain.NumberGenerator; | ||
import domain.NumberGeneratorImpl.RandomNumberGenerator; | ||
import java.util.List; | ||
import view.InputView; | ||
import view.OutputView; | ||
|
||
public class CarRaceApp { | ||
public static void main(String[] args) { | ||
final InputView inputView = new InputView(); | ||
final OutputView outputView = new OutputView(); | ||
final CarNameParser carNameParser = new CarNameParser(); | ||
final NumberGenerator numberGenerator = new RandomNumberGenerator(); | ||
|
||
List<Car> cars = getCars(outputView, inputView, carNameParser); | ||
int gameRounds = getGameRounds(outputView, inputView); | ||
final CarRace carRace = new CarRace(cars, gameRounds, numberGenerator); | ||
carRace.start(); | ||
|
||
outputView.printGameResult(carRace.getGameRoundsOutput()); | ||
outputView.printWinnerCarNames(carRace.getWinnerCarNames()); | ||
} | ||
|
||
private static int getGameRounds(OutputView outputView, InputView inputView) { | ||
outputView.printInputGameRounds(); | ||
return inputView.readGameRounds(); | ||
} | ||
|
||
private static List<Car> getCars(OutputView outputView, InputView inputView, | ||
CarNameParser carNameParser) { | ||
outputView.printInputCarsName(); | ||
String carNames = inputView.readCarNames(); | ||
return carNameParser.parse(carNames).stream().map(Car::new).toList(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package domain; | ||
|
||
import exception.CarNameTooLongException; | ||
|
||
public class Car { | ||
private String name; | ||
private int distance = 0; | ||
private final int MOVE_STANDARD_NUMBER = 4; | ||
private final int MAX_CAR_NAME_LENGTH = 5; | ||
|
||
public Car(String name){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 생성자에서 validate 함수를 호출하는건 좋은 것 같아요 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 특정 값을 입력받을 때, 입력 단계( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 좋은 접근 방법입니다. 👍 유효성을 검증하는 것 또한 일종의 책임입니다. 그리고 책임이 어딨는지 알기 도와주는 방법은 유사한 요구사항이 변경되었을 떄, 그리고 그 책임을 맡는 클래스가 바뀌었을 때 를 생각해보면 됩니다. View는 입출력에 대한 책임, Car는 자동차 라는 도메인 모델에 대한 책임을 갖고 있습니다. 만약 입력이 console이 아닌 웹 요청으로 바뀌면 어떨까요? 아니면 gui의 클릭으로 바뀐다던가? 반대로, 객체 규칙 결론적으로 생각해주신 방향과 저도 동일하게 생각합니다! |
||
validate(name); | ||
this.name = name; | ||
} | ||
|
||
private void validate(String name) { | ||
if(name.length() > MAX_CAR_NAME_LENGTH ){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. discord에서 이야기했던 것처럼 포맷팅을 한번 해보면 좋을 것 같아요. xml을 등록하고 reformat code를 하면 알아서 다 바뀐답니당 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 적용해보겠습니다! |
||
throw new CarNameTooLongException("자동차의 이름은 5글자 이하여야합니다."); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 어떤건 customException이고 어떤건 그냥 IllegalArgException을 쓰셨군요. 기준이 어떤건가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 자동차 이름의 길이가 특정 글자수를 초과하는 경우는 자동차 모델의 특징에 해당해서 예외를 구분해주는 것이 더 좋다고 생각했습니다. null 값에 대한 예외는 예상 가능한 예외라 생각하여 IllegalArgumentException을 사용해 처리해도 될것이라 생각했습니다. |
||
} | ||
if(name.trim().isEmpty()){ | ||
throw new IllegalArgumentException("자동차의 이름은 공백이 될 수 없습니다."); | ||
} | ||
} | ||
|
||
public void tryMoveByNumber(int number){ | ||
if(number >= MOVE_STANDARD_NUMBER){ | ||
move(); | ||
} | ||
} | ||
|
||
private void move(){ | ||
distance+=1; | ||
} | ||
|
||
public int getDistance(){ | ||
return distance; | ||
} | ||
|
||
public String getName(){ | ||
return name; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package domain; | ||
|
||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
public class CarNameParser { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CarNameparser는 어떤 의도로 만들었고, 지금상황에서 필요할까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
이러한 의도를 가졌었는데, 의도가 적절한 것일까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 의도를 가지고 분리했다면 좋다고 생각합니다. 각 클래스가 얼마나 역할에 대한 책임을 가질지는 늘 어렵습니다. 특히 상황에 따라 이게 변경되기도 하니까 말이죠. 저는 앞서 코멘트를 남겼듯이 책임을 분리할 때 하지만, 객체지향에서 간과되는 점이 있는 것은 새로운 클래스를 만드는 것 그리고 과도한 추상화 또한 관리 비용입니다. 아마 이 말이 잘 안 와닿을 수도 있을 것 같지만 그렇습니다. 오버엔지니어링이란 키워들르 한 번 알아보시면 좋을 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추상화와 클래스를 쪼개는 것은 미래에 코드가 변경되었을 때 이 변경을 쉽게하기 위해 미리 현재에 지불하는 비용 같은 것입니다. 앞서 말했듯이 추후에 파싱로직이 복잡해지거나, 추가 되는 경우엔 잘 쪼갰다고 볼 수 있겠죠. 하지만, 그런 경우가 안 온다면 어떨까요? 갑자기 그냥 파싱로직이 없어지거나 애당초 입력을 할 때 객체처럼 들어온다면? 그런 클래스를 분리할 시간에, 우리는 다른 코드 한 자 치는게 더 나은 선택일 수도 있습니다. 개발자의 시간은 곧 돈과 다름 없거든요. 그렇기에 우리는 |
||
private final String DELIMITER=","; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 값이 불변이라면 다른 방식으로 표현해보면 어떨까요?? static을 붙인 변수와 붙이지 않은 변수는 실제로 어떤 차이가 있나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
public List<String> parse(String carNames) { | ||
return Arrays.stream(carNames.trim().split(DELIMITER)) | ||
.map(String::trim) | ||
.toList(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package domain; | ||
|
||
import java.util.List; | ||
|
||
public class CarRace { | ||
|
||
private final List<Car> cars; | ||
private final int gameRounds; | ||
private final NumberGenerator numberGenerator; | ||
private final StringBuilder gameRoundsOutput = new StringBuilder("\n실행결과"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 실행결과와 같은 문구, 이 output과 관련된 내용은 다른 클래스에 옮겨보는게 어떨까요? |
||
private final String DISTANCE_EXPRESSION = "-"; | ||
private List<String> winnerCarNames; | ||
|
||
public CarRace(List<Car> cars, int gameRounds, NumberGenerator numberGenerator) { | ||
this.cars = cars; | ||
this.gameRounds = gameRounds; | ||
this.numberGenerator = numberGenerator; | ||
} | ||
|
||
public void start() { | ||
playRounds(); | ||
int maxDistance = getMaxDistanceFromCars(); | ||
winnerCarNames = getWinnerCarNamesForMaxDistance(maxDistance); | ||
} | ||
|
||
private void playRounds() { | ||
for (int i = 0; i < gameRounds; i++) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
요런 요구사항이 있었습니다. 왜 이런 요구사항이 있을까요? 그리고 한번 지켜보면 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드를 읽는 사람의 입장에서 indent의 깊이가 깊어질수록 가독성이 나쁘고, 로직이 복잡해진다고 느낍니다. 그러면 유지보수가 힘들어지기 때문인 것 같습니다. |
||
cars.forEach(car -> { | ||
car.tryMoveByNumber(numberGenerator.generate()); | ||
recordOutput(car); | ||
}); | ||
gameRoundsOutput.append("\n"); | ||
} | ||
} | ||
|
||
private void recordOutput(Car car) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 요것도 출력과 관련있는 내용인 것 같아요. 만약에, 저희가 gui를 제공하도록 바뀌어야한다고 하면 어디가 영향을 받을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추후 출력이 변경되었을 때의 생각을 해보면 됩니다. 현재 상태라면 만약 출력형식이 gui 또는 웹 응답 형태가 되었을 때, CarRace의 코드 또한 바뀌어야 할 것입니다. 이러한 형태는 그리 좋지 못합니다. 우리가 생각했을 때 출력에 대한 책임은 OutputView가 온전히 들고있어야 하는데, 그렇지 못하단 이야기거든요. CarRace도 들고있는 것이죠. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
좀더 핑퐁을 하면서 유도하고 싶지만 우리는 시간이 많지 않으니 😅 view가 domain형태로 된 객체를 직접 다룸으로써 출력에 대한 책임을 온전히 가질 수 있습니다. 이에 대해서 깊게 생각해보시면 좋을 것 같습니다. 이번 단원의 주요한 내용이거든요. 4단계의 힌트에 대해서 파보는 걸 추천드립니다. |
||
gameRoundsOutput.append("\n").append(car.getName()).append(" : ") | ||
.append(DISTANCE_EXPRESSION.repeat(car.getDistance())); | ||
} | ||
|
||
private int getMaxDistanceFromCars() { | ||
return cars.stream().mapToInt(Car::getDistance).max().orElse(0); | ||
} | ||
|
||
private List<String> getWinnerCarNamesForMaxDistance(int maxDistance) { | ||
return cars.stream().filter(car -> car.getDistance() == maxDistance).map(Car::getName) | ||
.toList(); | ||
} | ||
|
||
public List<String> getWinnerCarNames() { | ||
return winnerCarNames; | ||
} | ||
|
||
public String getGameRoundsOutput() { | ||
return gameRoundsOutput.toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package domain; | ||
|
||
public interface NumberGenerator { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NumberGenerator라는 Interface를 만들었군요 혹시 이 Interface는 어떻게 쓰이나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 추상화를 위한 목적보다는 랜덤한 값을 생성하는 메서드를 테스트 가능하게 만들기 위해 인터페이스로 분리했습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
int generate(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package domain.NumberGeneratorImpl; | ||
|
||
import domain.NumberGenerator; | ||
import java.util.Random; | ||
|
||
public class RandomNumberGenerator implements NumberGenerator { | ||
private final int MAX_RANDOM_NUMBER = 10; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위의 값들은 static을 안 썼고, 아래 Random은 static을 썼군요. 어떤 차이가 있나요? 그리고 자바에서 static이 붙고 안붙는게 변수이름에 어떤식으로 적용이 될까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. static을 붙이면 해당 객체를 생성할 때 하나의 인스턴스를 공유합니다. 상수로 작성하려했다면 static을 붙여야했습니다.
|
||
private final int MIN_RANDOM_NUMBER = 0; | ||
private static final Random random = new Random(); | ||
|
||
@Override | ||
public int generate() { | ||
return random.nextInt(MAX_RANDOM_NUMBER - MIN_RANDOM_NUMBER) + MIN_RANDOM_NUMBER; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package exception; | ||
|
||
public class CarNameTooLongException extends IllegalArgumentException { | ||
public CarNameTooLongException(String message) { | ||
super(message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package view; | ||
|
||
import java.util.InputMismatchException; | ||
import java.util.Scanner; | ||
|
||
public class InputView { | ||
Scanner scanner = new Scanner(System.in); | ||
|
||
public String readCarNames() { | ||
return scanner.nextLine(); | ||
} | ||
|
||
public int readGameRounds() { | ||
try { | ||
return scanner.nextInt(); | ||
} catch (InputMismatchException e) { | ||
throw new IllegalArgumentException("정수가 아닌 입력입니다."); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package view; | ||
|
||
import java.util.List; | ||
|
||
public class OutputView { | ||
private final String WINNER_DELIMITER = ", "; | ||
|
||
public void printInputCarsName(){ | ||
System.out.println("경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분)."); | ||
} | ||
|
||
public void printInputGameRounds(){ | ||
System.out.println("시도할 회수는 몇회인가요?"); | ||
} | ||
|
||
public void printGameResult(String gameResult){ | ||
System.out.println(gameResult); | ||
} | ||
|
||
public void printWinnerCarNames(List<String> carNames){ | ||
System.out.print(String.join(WINNER_DELIMITER, carNames)); | ||
System.out.println("가 최종 우승했습니다."); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.mockito.Mockito.when; | ||
|
||
import domain.Car; | ||
import domain.CarNameParser; | ||
import domain.CarRace; | ||
import domain.NumberGenerator; | ||
import java.io.ByteArrayOutputStream; | ||
import java.io.PrintStream; | ||
import java.util.List; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mock; | ||
import org.mockito.MockitoAnnotations; | ||
import view.OutputView; | ||
|
||
public class CarRaceAppTest { | ||
@Mock | ||
NumberGenerator randomNumberGenerator; | ||
|
||
@Test | ||
@DisplayName("자동차 경주 애플리케이션 출력 테스트") | ||
void carRaceTest() { | ||
// Given | ||
MockitoAnnotations.openMocks(this); | ||
int gameRounds = 3; | ||
String carNamesInput = "a,b"; | ||
CarNameParser carNameParser = new CarNameParser(); | ||
List<Car> cars = carNameParser.parse(carNamesInput).stream().map(Car::new).toList(); | ||
when(randomNumberGenerator.generate()).thenReturn(1, 5, 1, 5, 1, 5); | ||
|
||
ByteArrayOutputStream outContent = new ByteArrayOutputStream(); | ||
System.setOut(new PrintStream(outContent)); | ||
OutputView outputView = new OutputView(); | ||
CarRace carRace = new CarRace(cars, gameRounds, randomNumberGenerator); | ||
|
||
// When | ||
carRace.start(); | ||
outputView.printGameResult(carRace.getGameRoundsOutput()); | ||
outputView.printWinnerCarNames(carRace.getWinnerCarNames()); | ||
|
||
// Then | ||
String output = outContent.toString(); | ||
String[] lines = output.split("\\R"); | ||
assertThat(lines).containsExactly( | ||
"", | ||
"실행결과", | ||
"a : ", | ||
"b : -", | ||
"", | ||
"a : ", | ||
"b : --", | ||
"", | ||
"a : ", | ||
"b : ---", | ||
"", | ||
"b가 최종 우승했습니다." | ||
); | ||
|
||
System.setOut(System.out); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package domain; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
import java.util.List; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
|
||
public class CarNameParserTest { | ||
@ParameterizedTest | ||
@ValueSource(strings = {"a,b,c", " a, b,c "," a , b, c"}) | ||
@DisplayName("자동차 문자열 파싱 테스트") | ||
void splitCarNamesString(String carNamesInput) { | ||
// Given | ||
CarNameParser carNameParser = new CarNameParser(); | ||
List<String> expected = List.of("a", "b", "c"); | ||
|
||
// When | ||
List<String> actual = carNameParser.parse(carNamesInput); | ||
|
||
// Then | ||
assertThat(actual).isEqualTo(expected); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 mockito를 쓰셨군요...