자동차 경주 피드백
Car
public class Car {
private int distance;
public Car() {
this.distance = 0;
}
public int distance() {
return distance;
}
// 람다식으로 표현할 수 있도록 GameStrategy를 Funtional Inteface로 만들었는데 이 부분에 대해 따봉 받았다!
// Predicate Interface와 고민했는데 movable 메서드로 좀 더 의도적으로 표현하기 위해 GameStrategy Interface를 만들었다.
public void forward(GameStrategy racingStrategy) {
if (racingStrategy.movable()) {
distance++;
}
}
// 도메인 모델에서 출력(Presentation Layer)과 결합이 생겼다고 하셨다.
// View의 경우, 출력 형식에 따라 변경이 잦기도 하고, Console -> Web 등으로 구조적 변경이 이루어질 경우 도메인 모델의 변경이 불가피하기 때문에 의존을 제거하자!
// 이 부분에 대해서 ResultView로 빼야겠다.
public void printDistance() {
String printedDistance = "";
for (int i = 0; i < distance; i++) {
printedDistance += "-";
}
System.out.println(printedDistance);
}
}
ResultView
public class ResultView {
// 응답결과를 표현하는 Presentation Layer 역할을 main 메서드를 호출하는 application 역할을 수행하도록 만들어 버렸다.
// Racing을 ResultView로 만드는 것이 더 나은 표현일 것 같다.
public static void main(String[] args) {
Racing racing = new Racing(InputView.inputCarNumber(), InputView.inputTryNumber());
racing.play();
}
}
InputViewTest
// reflection을 활용하여 테스트하기 보다는 System.setIn 메서드 등을 통해서 Scanner input 값을 세팅하고 테스트하는 방향도 있다.
// private 메서드에 대해 테스트가 필요하다면 객체를 분리할 시점은 아닌지 고민도 해보는 것이 좋다고 하셨다.
// 이 부분은 util.NumberValidator로 만들어 분리해야 겠다.
@ParameterizedTest(name = "입력한 수에 따라 양수이면 true, 아니면 false 확인 테스트")
@CsvSource(value = {"1:true", "7:true", "0:false", "-1:false"}, delimiter = ':')
public void givenNumber_WhenValidateNumber_ThenTrueOrFalse(int input, boolean expected) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
assertThat(getIsPositive().invoke(null, input)).isEqualTo(expected);
}
private Method getIsPositive() throws NoSuchMethodException {
Method method = InputView.class.getDeclaredMethod("isPositive", int.class);
method.setAccessible(true);
return method;
}
자동차 경주 리팩토링
InputView로부터 분리한 NumberValidator
public class NumberValidator {
private static final int POSITIVE_MINIMUM = 1;
private NumberValidator() {
}
public static int validatedPositive(int number) {
if (!isPositive(number)) {
throw new IllegalArgumentException("1회 이상만 입력 가능합니다.");
}
return number;
}
private static boolean isPositive(int number) {
return number >= POSITIVE_MINIMUM;
}
}
util 패키지에 NumberValidator 유틸성 클래스로 분리함으로써 테스트가 하기 쉬운 구조로 만들었다.
NumberValidatorTest
public class NumberValidatorTest {
@ParameterizedTest(name = "양수를 입력하는 경우 해당 양수 반환 테스트")
@ValueSource(ints = {1, 3, 6, 9})
public void validatedPositive_양수_반환(int input) {
assertThat(validatedPositive(input)).isEqualTo(input);
}
@ParameterizedTest(name = "0이나 음수를 입력하는 경우 IllegalArgumentException 발생 테스트")
@ValueSource(ints = {0, -1, -7, -9})
public void validatedPositive_IllegalArgumentException_발생(int input) {
assertThatThrownBy(() -> validatedPositive(input))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("1회 이상만 입력 가능합니다.");
}
}
따로 private 메서드를 public으로 만들어야 하나 고민할 필요도 없어졌고, 테스트하는데에도 훨씬 수월해졌다.
RacingGame
public class RacingGame {
private final Cars cars;
public RacingGame(int carNumber) {
this.cars = initialCars(carNumber);
}
public GameResult play(int tryNumber) {
GameResult result = new GameResult();
for (int i = 0; i < tryNumber; i++) {
cars.forwardCars();
result.addRecord(Cars.copyCars(cars));
}
return result;
}
private static Cars initialCars(int carNumber) {
List<Car> cars = new ArrayList<>();
for (int i = 0; i < carNumber; i++) {
cars.add(new Car());
}
return new Cars(cars);
}
}
InputView에서 사용자의 입력값을 받아 메시지를 RacingGame에 메시지를 보내 자동차게임 로직이 돌아가도록 구성했다.
그리고 이후 GameResult에서 매 회 턴들에 대해 기록을 하고 결과를 낼 수 있도록 저장하기 위해 각 Cars 객체를 copy하여 저장할 수 있도록 copyCars 메서드를 작성했다.
copyCars 메서드
public static Cars copyCars(Cars cars) {
return new Cars(cars.cars().stream()
.map(Cars::copyCar)
.collect(Collectors.toList()));
}
private static Car copyCar(Car car) {
return new Car(car.distance());
}
RacingGameTest
public class RacingGameTest {
private Cars cars;
@BeforeEach
public void setUp() {
List<Car> carList = new ArrayList<>();
carList.add(new Car());
carList.add(new Car());
carList.add(new Car());
cars = new Cars(carList);
}
@Test
public void play_3번_시도() {
cars.forwardCars();
cars.forwardCars();
cars.forwardCars();
assertThat(cars.cars().get(0).distance()).isBetween(0, 3);
}
}
RacingGameTest에서는 3개의 자동차를 cars에 저장하여 forwardCars()
메서드를 통해 랜덤으로 전진하고 0번째 엘리먼트가 0 ~ 3만큼 전진했는지 확인하는 테스트를 작성했다.
GameResult
public class GameResult {
private final List<Cars> record;
public GameResult() {
record = new ArrayList<>();
}
public List<Cars> record() {
return Collections.unmodifiableList(record);
}
public void addRecord(Cars cars) {
record.add(cars);
}
}
그리고 GameResult에는 매회 턴마다의 Cars 객체를 저장할 수 있도록 작성하여 추후에는 최종승리자를 판별할 수 있는 메서드를 추가할 예정이다.
ResultView
public class ResultView {
private ResultView() {}
public static void show(GameResult result) {
System.out.println();
System.out.println("실행 결과");
for (Cars cars : result.record()) {
printDistance(cars);
System.out.println();
}
}
private static void printDistance(Cars cars) {
cars.cars().forEach((car) -> System.out.println(car.printedDistance()));
}
}
다시 돌아온 ResultView에서는 InputView에서 입력값을 받아 RacingGame에서 자동차 게임 로직이 돌아가고 GameResult에서 매 턴 결과를 저장하여 다시 ResultView에서 그 결과들을 보여줄 수 있도록 구성했다.
RacingGameApplication
public class RacingGameApplication {
public static void main(String[] args) {
int carNumber = InputView.inputCarNumber();
int tryNumber = InputView.inputTryNumber();
RacingGame racingGame = new RacingGame(carNumber);
GameResult result = racingGame.play(tryNumber);
ResultView.show(result);
}
}
마지막 application으로 마무리하여 3단계를 통과했다! :)
'교육 및 책 > TDD, 클린 코드 with Java' 카테고리의 다른 글
TDD, 클린 코드 - 로또 (0) | 2023.05.14 |
---|---|
TDD, 클린 코드 - 로또(자동) (0) | 2023.04.23 |
TDD, 클린코드 - 문자열 사칙연산 계산기 (0) | 2023.04.19 |
TDD, 클린 코드 - 문자열 덧셈 계산기 (0) | 2023.04.08 |
TDD, 클린 코드 학습 테스트 (0) | 2023.04.05 |