본문 바로가기

강의/TDD, Clean Code with Java 12기

Step4 - 자동차 경주(우승자) (9일차, 10일차)

기능 요구사항

  • 각 자동차에 이름을 부여할 수 있다. 자동차 이름은 5자를 초과할 수 없다.
  • 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
  • 자동차 이름은 쉼표(,)를 기준으로 구분한다.
  • 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한명 이상일 수 있다.

실행 결과

  • 위 요구사항에 따라 3대의 자동차가 5번 움직였을 경우 프로그램을 실행한 결과는 다음과 같다.
경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).
pobi,crong,honux
시도할 회수는 몇회인가요?
5

실행 결과
pobi : -
crong : -
honux : -

pobi : --
crong : -
honux : --

pobi : ---
crong : --
honux : ---

pobi : ----
crong : ---
honux : ----

pobi : -----
crong : ----
honux : -----

pobi : -----
crong : ----
honux : -----

pobi, honux가 최종 우승했습니다.

 


힌트

  • 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
    • indent가 2이상인 메소드의 경우 메소드를 분리하면 indent를 줄일 수 있다.
    • else를 사용하지 않아 indent를 줄일 수 있다.
  • 자동차 이름을 쉼표(,)를 기준으로 분리하려면 String 클래스의 split(",") 메소드를 활용한다.

String[] names = inputName.split(",");

  • 사용자가 입력한 이름의 숫자 만큼 자동차 대수를 생성한다.
  • 자동차는 자동차 이름과 위치 정보를 가지는 Car 객체를 추가해 구현한다.

프로그래밍 요구사항

  • indent(인덴트, 들여쓰기) depth를 2를 넘지 않도록 구현한다. 1까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
  • 함수(또는 메소드)의 길이가 15라인을 넘어가지 않도록 구현한다.
    • 함수(또는 메소드)가 한 가지 일만 잘 하도록 구현한다.
  • 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.

기능 목록 및 commit 로그 요구사항

  • 기능을 구현하기 전에 README.md 파일에 구현할 기능 목록을 정리해 추가한다.
  • git의 commit 단위는 앞 단계에서 README.md 파일에 정리한 기능 목록 단위로 추가한다.

코드 작성

package step4;

public class Car {
    private int position;
    private String name;

    public Car(int position, String name) {
        this.position = position;
        this.name = name;
    }

    public void moveForward() {
        this.position++;
    }

    public int getPosition() {
        return position;
    }

    public String getName() {
        return name;
    }
}
package step4;

public class CarManager {
    private Car car;
    private RaceManager raceManager = new RaceManager();

    public CarManager(Car car) {
        this.car = car;
    }

    public void canIGo() {
        if (raceManager.judge()) {
            car.moveForward();
        }
    }

    public int getCurrCarPosition() {
        return car.getPosition();
    }

    public String getCurrCarName() {
        return car.getName();
    }
}
package step4;

import java.util.Scanner;

public class InputView {
    private static final String HOW_MANY_CAR = "경주할 자동차 이름을 입력하세요(이름은 쉼표(,)를 기준으로 구분).";
    private static final String HOW_MANY_CYCLE = "시도할 횟수는 몇 회 인가요?";
    private static final int MIN_ROUND = 0;
    private static final int MIN_CAR = 0;
    private static final String CYCLE_ERROR_MESSAGE = "시도할 횟수는 0보다 커야 합니다.";
    private static final String CAR_ERROR_MESSAGE = "자동차 대수는 1대 이상이어야 합니다.";
    private static final Scanner sc = new Scanner(System.in);

    public int getNumOfCycle() {
        System.out.println(HOW_MANY_CYCLE);
        return getInput();
    }

    public String getNameOfCar() {
        System.out.println(HOW_MANY_CAR);
        return getStringInput();
    }

    private int getInput() {
        return sc.nextInt();
    }

    private String getStringInput() {
        return sc.next();
    }

    public void inputCarCheck(int numOfCar) {
        System.out.println(numOfCar);
        if (numOfCar <= MIN_CAR) {
            throw new IllegalArgumentException(CAR_ERROR_MESSAGE);
        }
    }

    public void inputRoundCheck(int numOfCycle) {
        if (numOfCycle <= MIN_ROUND) {
            throw new IllegalArgumentException(CYCLE_ERROR_MESSAGE);
        }
    }
}
package step4;

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

public class Race {
    private static int numOfCar;
    private static int numOfCycle;
    private static String nameOfCar;
    private static final int INIT_CAR_POSITION = 1;

    private InputView inputView = new InputView();
    private ResultView resultView = new ResultView();
    private List<CarManager> carManagers;

    public Race(String nameOfCar, int numOfCycle) {
        this.nameOfCar = nameOfCar;
        this.numOfCar = getNumOfCar();
        this.numOfCycle = numOfCycle;
        validInputConfirm();
        prepareCars();
    }

    public int getNumOfCar() {
        return nameOfCar.split(",").length;
    }

    public void prepareCars() {
        String[] carNamesAry = nameOfCar.split(",");
        carManagers = new ArrayList<>();
        for (int i = 0; i < numOfCar; i++) {
            System.out.println(carNamesAry[i]);
            Car car = new Car(INIT_CAR_POSITION,carNamesAry[i]);
            carManagers.add(new CarManager(car));
        }
    }

    public void validInputConfirm() {
        inputView.inputCarCheck(numOfCar);
        inputView.inputRoundCheck(numOfCycle);
    }

    public void playRace() {
        for (CarManager carManager: carManagers) {
            carManager.canIGo();
        }
    }

    public void printResult() {
        for (CarManager carManager: carManagers) {
            resultView.showCurrCarName(carManager.getCurrCarName());
            resultView.showCurrCarPosition(carManager.getCurrCarPosition());
            resultView.findMaxPositionCarName(carManager.getCurrCarName(), carManager.getCurrCarPosition());
        }
    }


}
package step4;

import java.util.Random;
import java.util.Scanner;

public class RaceManager {
    private static final int DICE_THRETHOLD = 4;
    private static Random rd = new Random();

    public boolean judge() {
        int rdNumber = rd.nextInt(10);
        if (rdNumber >= 4) {
            return true;
        }
        return false;
    }
}
package step4;

import step3.model.Car;

public class ResultView {
    private static String maxCarName;
    private static int maxCarPosition = 0;

    public void showCurrCarPosition(int location) {
        for (int i = 0; i < location; i++) {
            System.out.print("-");
        }
        System.out.println();
    }

    public void showCurrCarName(String name) {
        System.out.print(name+": ");
    }

    public void findMaxPositionCarName(String carName, int carPosition) {
        if (carPosition > maxCarPosition) {
            maxCarName = carName;
            maxCarPosition = carPosition;
        } else if (carPosition == maxCarPosition) {
            maxCarName += ", "+carName;
        }
    }

    public void printFinalResult() {
        System.out.println(maxCarName+"가 최종 우승했습니다.");
    }
}
import step4.InputView;
import step4.Race;
import step4.ResultView;

import java.util.Scanner;

public class RacingCarMain {
    public static void main(String[] args) {
        InputView inputView = new InputView();
        String nameOfCar = inputView.getNameOfCar();
        int numOfCycle = inputView.getNumOfCycle();

        Race race = new Race(nameOfCar, numOfCycle);

        for (int i = 0; i < numOfCycle; i++) {
            race.printResult();
            race.playRace();
            System.out.println();
        }
        race.printResult();
        ResultView resultView = new ResultView();
        resultView.printFinalResult();
    }
}

 

unit test를 하는 것이 아직 어색하다.

추가적으로 모든 코드를 다 작성하고 모듈이 돌아가는 것을 확인한 뒤에 테스트 코드를 짜려고 하니 '이걸 왜 하는거지? 이미 코드는 잘 돌아가는데?' 라는 의구심이 생겨서  흥미도 없어지고, 작성 속도도 떨어지는 것 같다.

이래서 TDD 기반으로 테스트 코드를 작성하는 건가.... 싶은 생각이 든다.

 

테스트 코드를 어떻게 작성하는지 좀 더 살펴봐야 할 것 같다.