본문 바로가기

TDD이론과연습&리팩토링

자바 플레이그라운드 TDD 자동차 경주

728x90
반응형

기능 요구사항

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

실행 결과

  • 위 요구사항에 따라 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가 최종 우승했습니다.

힌트

  • 자동차는 자동차 이름과 위치 정보를 가지는 Car 객체를 추가해 구현한다.

프로그래밍 요구사항

  • 자바 코드 컨벤션을 지키면서 프로그래밍한다.
    • 기본적으로 Google Java Style Guide을 원칙으로 한다.
    • 단, 들여쓰기는 '2 spaces'가 아닌 '4 spaces'로 한다.
  • indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다. 2까지만 허용한다.
    • 예를 들어 while문 안에 if문이 있으면 들여쓰기는 2이다.
    • 힌트: indent(인덴트, 들여쓰기) depth를 줄이는 좋은 방법은 함수(또는 메소드)를 분리하면 된다.
  • else 예약어를 쓰지 않는다.
    • 힌트: if 조건절에서 값을 return하는 방식으로 구현하면 else를 사용하지 않아도 된다.
    • else를 쓰지 말라고 하니 switch/case로 구현하는 경우가 있는데 switch/case도 허용하지 않는다.
  • 3항 연산자를 쓰지 않는다.
  • 함수(또는 메소드)가 한 가지 일만 하도록 최대한 작게 만들어라.
  • 모든 기능을 TDD로 구현해 단위 테스트가 존재해야 한다. 단, UI(System.out, System.in) 로직은 제외
    • 핵심 로직을 구현하는 코드와 UI를 담당하는 로직을 구분한다.
    • UI 로직을 InputView, ResultView와 같은 클래스를 추가해 분리한다.
  • 모든 원시 값과 문자열을 포장한다.
  • 일급 컬렉션을 쓴다.

 


기능 요구 사항 정리

  •  자동차 이름을 체크한다.
    • 자동차 문자열은 , 를 기준으로 구분한다.
    •  자동차 이름은 5자를 초과할 수 없다.
    •  자동차는 전진할 수 있다.
  •  0에서 9사이의 random값을 구할 수 있다.
    •  전진하는 조건은 random값이 4이상일 때다.
  •  게임 완료 후 우승자를 알려준다.
    •  우승자는 한 명 이상일 수 있다.

1. 자동차 객체 (Car)

일급컬렉션을 사용하라

자동차 객체에 들어갈 이름과 위치를 각각 객체화한다.

Name.java

public class Name {

  private final String name;

  public Name(String name) {
    if (StringUtils.isBlank(name)) {
      throw new IllegalArgumentException("이름은 빈 값이 될 수 없습니다.");
    }
    if (name.trim().length() > 5) {
      throw new IllegalArgumentException("이름은 5자를 초과할 수 없습니다.");
    }
    this.name = name.trim();
  }

  public String getName() {
    return name;
  }
}

 

Position.java

public class Position {

  private int position;

  public Position(int position) {
    this.position = position;
  }


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

  public boolean lessThan(Position position) {
    return this.position < position.getPosition();
  }

  public boolean isSame(Position position) {
    return this.equals(position);
  }

  public int getPosition() {
    return position;
  }


  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Position position1 = (Position) o;
    return position == position1.position;
  }


  @Override
  public int hashCode() {
    return Objects.hash(position);
  }

  @Override
  public String toString() {
    return "Position{" + "position=" + position + '}';
  }

}

Position과 Name객체에서 validation을 체크하고, 필요한 로직을 구현함으로써 단일책임원칙(Car객체)을 지킨다.

 

public class Car {

  private final Name name;
  private final Position position;

  public Car(String name) {
    this(name, new Position(0));
  }

  public Car(String name, Position position) {
    this.name = new Name(name);
    this.position = position;
  }

  public void move() {
    position.move();
  }

  public Position getMaxposition(Position position) {
    if (this.position.lessThan(position)) {
      return position;
    }
    return this.position;
  }


  public boolean isWinner(Position position) {
    return this.position.isSame(position);
  }

  public String getName() {
    return name.getName();
  }

  public int getPosition() {
    return position.getPosition();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    Car car = (Car) o;
    return Objects.equals(name, car.name) && Objects.equals(position,
        car.position);
  }

  @Override
  public int hashCode() {
    return Objects.hash(name, position);
  }

  @Override
  public String toString() {
    return "Car{"
        + "name='" + name + '\''
        + ", position=" + position + '}';
  }

}

Car객체에서는 Name와 Positon객체를 final을 이용해서 불변객체로 만든다.

조금 더 안전한 코드가 된다.

 

 

2. 자동차 목록을 가지고 있는 객체

cars.java

public class Cars {

  private static final int MOVE_CONDITION = 4;
  private static final int MAX_BOUND = 10;
  private final List<Car> cars = new ArrayList<>();

  public Cars(String names) {
    if (StringUtils.isBlank(names)) {
      throw new IllegalArgumentException("이름은 빈값이 올 수 없습니다.");
    }

    for (String name : names.split(",")) {
      cars.add(new Car(name));
    }
  }

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

  public void moveCars() {
    for (Car car : cars) {
      if (movable()) {
        car.move();
      }
    }
  }

  public int generateRanomNo() {
    return new Random().nextInt(MAX_BOUND);
  }

  public boolean movable() {
    return generateRanomNo() > MOVE_CONDITION;
  }

  public List<Car> findWinners() {
    Position position = new Position(0);
    for (Car car : cars) {
      position = car.getMaxposition(position);
    }

    List<Car> winnerList = new ArrayList<>();
    for (Car car : cars) {
      if (car.isWinner(position)) {
        winnerList.add(car);
      }
    }

    return winnerList;
  }
}

자동차 race중 이동이 필요할 때 체크하는 로직도 분리가능하다.

winner를 찾는 로직에서 getMaxposition에 position객체를 던짐으로 Position객체에서 확인하도록 한다.

 

3. 자동차 경주 객체

public class RacingCar {
  private int tryNo;
  private final Cars cars;

  public RacingCar(String carNames, int tryNo) {
    cars = initCars(carNames);
    this.tryNo = tryNo;
  }

  private Cars initCars(String carNames) {
    return new Cars(carNames);
  }

  public List<Car> race() {
    this.tryNo--;
    cars.moveCars();
    return cars.getList();
  }

  public List<Car> findWinner() {
    return cars.findWinners();
  }

  public boolean isEnd() {
    return tryNo == 0;
  }
}

 

4. 출력객체

public class ResultView {

  public void printRace(List<Car> cars) {
    for (Car car : cars) {
      StringBuilder sb = new StringBuilder();
      for (int i = 0; i < car.getPosition(); i++) {
        sb.append("-");
      }
      System.out.println(car.getName() + " : " + sb);
    }
    System.out.println();
  }

  public void printWinners(List<Car> winners) {
    List<String> winnerNames = winners.stream().map(Car::getName).collect(Collectors.toList());
    System.out.println(String.join(",", winnerNames) + "가 최종 우승했습니다.");
  }
}

 

view클래스와 실제 행동을 위한 객체는 분리하는 것이 좋다.

 

 

 

정답은 아니다.

 

728x90
반응형