
우테코 프리코스 2차
2차가 드디어 시작되었다. 수요일에 바로 올 줄 알았는데 수요일 오후 3 시쯔음 과제가 주어졌다. 수요일 과제를 기다리다 과제가 오지 않아서 괜히 내가 전 과제에서 무언가 실수했나? 생각에 괜히 마음이 불안했다. 어찌됐든! 이번 주제는 자동차 경주 게임이었다.
이번 기능 요구사항이었다.
그리고 프로그래밍 요구사항은 다음과 같았다.
확실히 클린코드를 향해 달려 나가는 과제인 것 같다.
구현할 기능부터 일단 적었다.
이번 과제에서는 생겼던 이슈가 많았다.
- 어떻게 랜덤 값으로 전진하는 차를 테스트할까?
- 레이스 현 상황 출력과 서비스 로직을 어떻게 분리할까?
- 컬렉션이 많으니 코드가 많이 지저분해 보인다. 어떻게 개선할까?
어떻게 랜덤 값으로 전진하는 차를 테스트할까?
레이스 클래스를 테스트하고 싶었지만 랜덤 방식으로 돌아가는 코드를 테스트하기에 불가능해 보였다. 최근에 공부하고 있는 디자인 패턴을 사용해 보았다.
맨 처음은 template method pattern을 사용해 보았다.
해결 시도 1. template method pattern
public abstract class Race {
protected List<Car> cars;
private int round;
public Race(List<Car> cars, int round) {
this.cars = new ArrayList<>(cars);
this.round = round;
}
public List<String> start() {
for (int i = 0; i < round; i++) {
makeCarRun();
}
return RaceResult.getResult(cars);
}
protected abstract void makeCarRun(); //달리는 방식은 자식 객체에서 정한다.
}
public class RandomRace extends Race {
private static final int MIN_RANDOM_DIGIT = 0;
private static final int MAX_RANDOM_DIGIT = 9;
public RandomRace(List<Car> cars, int round) {
super(cars, round);
}
@Override
protected void makeCarRun() {
cars.forEach(car -> car.run(randomly()));
}
private int randomly() {
return RandomUtils.nextInt(MIN_RANDOM_DIGIT, MAX_RANDOM_DIGIT);
}
}
템플릿 메서드 패턴으로 하게 되면 문제점이 있다. cars에 접근하기 위해 cars를 protected로 변환 시켰다. 이 의미는 자식 객체에서 컬렉션에 접근이 가능하다는 것이다. (불변성 유지가 힘들어 보임) 따라서 템플릿 메서드 패턴은 포기했다.
해결 시도 2. Strategy pattern
public class Race {
private List<Car> cars;
private int round;
public Race(List<Car> cars, int round) {
this.cars = new ArrayList<>(cars);
this.round = round;
}
public List<String> start(DigitStrategy digitStrategy) {
for (int i = 0; i < round; i++) {
makeCarRun(digitStrategy);
}
return RaceResult.getResult(cars);
}
private void makeCarRun(DigitStrategy digitStrategy){
cars.forEach(car -> car.run(digitStrategy.getDigit()));
}
}
Strategy 부분 :
public interface DigitStrategy {
int getDigit();
}
public class InputDigit implements DigitStrategy {
private int inputNumber;
public InputDigit(int inputNumber){
this.inputNumber = inputNumber;
}
@Override
public int getDigit() {
return inputNumber;
}
}
public class RandomDigit implements DigitStrategy {
private static final int MIN_RANDOM_DIGIT = 0;
private static final int MAX_RANDOM_DIGIT = 9;
@Override
public int getDigit() {
return RandomUtils.nextInt(MIN_RANDOM_DIGIT, MAX_RANDOM_DIGIT);
}
}
만일 테스트를 하고 싶다면 매개변수로 InputDigit을 넣으면 확인이 가능했고 실제 서비스는 랜덤방식인 RandomDigit을 넣어 사용하면 된다.
레이스 현 상황 출력과 서비스 로직을 어떻게 분리할까?
pobi : --
woni : ----
jun : ---
각 차수별(매 Round) 위와 같이 화면에 현 위치를 그래프 형식으로 표시해야 했다. 서비스 로직은 다음과 같았다.
- 매 차수별 모든 참여한 차에게 랜덤한 값을 부여
- 매 차수별 현 위치 출력
서비스 로직에 출력이 있는 것이 마음에 들지 않았다. 어떻게 출력을 분리할 수 있을까? 결론은 로그를 저장해서 경기가 끝나면 모든 로그를 화면에 출력하는 방식으로 진행했다.
public class RaceLog {
private static final String ENTER = "\n";
private static final String LOG_FORM = "%s : %s";
private static final String GRAPH = "-";
private StringBuilder log;
private RaceLog() {
this.log = new StringBuilder();
}
public static RaceLog newInstance() {
return new RaceLog();
}
public void writeLog(Participants participants) {
participants.getParticipants()
.forEach(this::logCurrentPosition);
log.append(ENTER);
}
private void logCurrentPosition(Car car) {
String name = car.getName();
int currentPosition = car.getCurrentPosition();
String graph = mapToGraph(currentPosition);
log.append(String.format(LOG_FORM, name, graph));
log.append(ENTER);
}
private String mapToGraph(int currentPosition) {
StringBuilder graph = new StringBuilder();
for (int i = 0; i < currentPosition; i++) {
graph.append(GRAPH);
}
return graph.toString();
}
public String getLog() {
return log.toString();
}
}
로그는 계속해서 concat이 이뤄져야 한다. String으로 계속 concat을 하게 되면 새로운 객체가 계속 생겨 효율이 좋지 않아서 StringBuilder 클래스를 사용했다.
컬렉션이 많으니 코드가 많이 지저분해 보인다. 어떻게 개선할까?
List
이러한 클래스들을 다시 한 번 추상화를 해줬다. List
배운 점
이번 과제에서 배운 점이 굉장히 많았다. 어떻게 보면 나의 코딩의 기준을 하나씩 세워 나갔던 과제이었다.
- 디자인 패턴의 중요성을 배웠다.
- 가독성이 좋게 하기 위해 Wrapping을 사용하자. (컬렉션이 보인다면 추상화가 가능한 지 바로 고민해보자)
- 입력과 출력을 어떻게 나눠야할 지 감이 잡혔다.
- 2 번과 비슷하지만 모든 객체지향의 시작은 추상화부터 시작이 되는 것 같다.