[자동차 경주] 피드백 정리

berry·2023년 4월 3일
0

우아한테크코스

목록 보기
3/3

우테코 레벨1 첫 번째 미션이었던 자동차 경주 미션 피드백 중 기억하면 좋을 점을 정리하려 한다.

페어: 해시🍟
리뷰어: 제리 리뷰어님 (vagabond95)

step1 PR 링크
https://github.com/woowacourse/kotlin-racingcar/pull/41
step2 PR 링크
https://github.com/woowacourse/kotlin-racingcar/pull/71


1. 인터페이스로 느슨한 연결해주기

배경

자동차가 전진하는 조건은 0에서 9 사이에서 무작위 값을 구한 후 무작위 값이 4 이상일 경우이다.
따라서 무작위 값을 생성하는 클래스를 만들어줘야 한다.

이때 생각해야 될 점은, 만약 프로그램이 확장되거나 수정된다면 랜덤값이 아니라 다른 방식으로 숫자를 생성해야 될 수도 있다!

이런 경우를 대비해서 CarNumberGenerator 라는 인터페이스를 만든 후,
이를 구현하는 RandomNumberGenerator를 프로그램에 주입해주면 느슨한 결합을 통해 더욱 확장성 있는 프로그램이 가능하다!

적용하기

컨트롤러에서 CarNumberGenerator 타입으로 랜덤 숫자 생성기를 주입받는다.

Application.kt

val controller = Controller(InputView(InputValidator()), OutputView(), RandomNumberGenerator())

Controller.kt

class Controller(
     private val inputView: InputView,
     private val outputView: OutputView,
     private val generator: CarNumberGenerator
 )

2. 전략패턴

배경

도메인 로직 테스트를 위해 랜덤값 대신 원하는 값을 반환하는 TestNumberGenerator 클래스를 만들어서 테스트코드를 작성했다.

class TestNumberGenerator(private val number: Int) : CarNumberGenerator {
        override fun generate(): Int {
            return number
        }
    }

이에 대해 리뷰어님이 전략패턴을 잘 활용해주셨네요! 라는 코멘트를 남겨주셨다.
전략패턴을 의도하지 않았기 때문에... 어리둥절... 암튼 전략패턴의 개념을 정리해보자!😅

전략패턴 이란?
알고리즘 및 로직을 따로 정의하여 필요에 의해 사용 또는 교체 할 수 있는 패턴.

  • 여러 알고리즘을 캡슐화하고 상호 교환 가능하게 만드는 패턴
  • 컨텍스트에서 사용할 알고리즘을 클라이언트가 선택한다.

예를 들어 덧셈, 뺄셈 계산기 총 2가지의 계산기가 있다고 하자~
strategy pattern을 적용하면 다음과 같다~
덧셈, 뺄셈 계산기는 calculator 인터페이스를 구현한다.

클라이언트는 calculator를 가지고 있고, 덧셈, 뺄셈 중 어떤 계산기인지 set을 이용해서 바꿔준다.(자바 코드)
클라이언트의 operate() 함수에서는 calculator의 execute()를 실행한다.

1. Calculator interface

public interface Calculator {
    double execute(double n1, double n2);
}

2. PlusCalculator class

public class PlusCalculator implements Calculator{
    @Override
    public double execute(double n1, double n2) {
        return n1+n2;
    }
}

3. class MinusCalculator

public class MinusCalculator implements Calculator{
 
    @Override
    public double execute(double n1, double n2) {
        return n1-n2;
    }
}

▶ PlusCalculator와 MinusCalculator은 Calculator를 상속받아 각자의 알고리즘을 구현

4. People class

public class People {
 
    private Calculator calculator;
    private double n1;
    private double n2;
 
    public double operate(){
        return calculator.execute(n1,n2);
    }
 
    public void setCalculator(Calculator calculator){
        this.calculator=calculator;
    }
 
    void changeNumber(double n1, double n2) {
        this.n1 = n1;
        this.n2 = n2;
    }
 
}

▶ setCalculator()을 이용해서 원하는 계산기로 바꿀 수 있다.

5. Main class

public class Main {
    public static void main(String[] args) {
        People people = new People();
 
        // 숫자 설정
        people.changeNumber(1,2);
 
        // Calculator 설정
        people.setCalculator(new PlusCalculator());
        double result1 = people.operate();
        System.out.println(result1);
 
 
        // 새로운 Calculator 설정
        people.setCalculator(new MinusCalculator());
        double result2 = people.operate();
        System.out.println(result2);
    }
}

적용하기

내가 짠 코드에 이 패턴을 적용해서 이해해보자~
Car 클래스의 tryMove 함수는 CarNumberGenerator 타입을 인자로 받고, 함수 내에서는 인터페이스의 generate 함수로 생성된 int 값에 따라 전진을 판단한다.

CarNumberGenerator.kt

interface CarNumberGenerator {
    fun generate(): Int
}

실제 코드에서는 랜덤값을 생성하는 RandomNumberGenerator 클래스를 주입받는다.

RandomNumberGenerator.kt

class RandomNumberGenerator : CarNumberGenerator {
    override fun generate() = (RANDOM_MIN_NUMBER..RANDOM_MAX_NUMBER).random()

    companion object {
        private const val RANDOM_MIN_NUMBER = 0
        private const val RANDOM_MAX_NUMBER = 9
    }
}

테스트 코드에서는 원하는 숫자를 생성하는 TestNumberGenerator 클래스를 주입받는다.

class TestNumberGenerator(private val number: Int) : CarNumberGenerator {
        override fun generate(): Int {
            return number
        }
    }

즉, 클라이언트에서 랜덤값 생성기를 선택하냐 테스트값 생성기를 선택하냐에 따라 generate 알고리즘이 교체된다.
전략 패턴을 사용했다고 볼 수 있다~😊

3. 도메인 모델의 제약사항은 도메인에서

배경

자동차 이름이 5글자 미만이어야 한다는 제약사항이 있다.
이름은 사용자에게 입력받기 때문에, InputValidator 에서 글자 수를 검사해서 5글자 이상이라면 예외를 발생시켰다.

이에 대해 다음과 같은 리뷰가 달렸다

자동차의 이름이 5글자 미만이어야 하는 것은 자동차의 역할일까요? Validator의 역할일까요?
예를 들어 다음 코드는 잘 동작을 할까요?

Car(name = "")
Car(name = "123456789")

적용

자동차 이름은 자동차의 제약사항이기 때문에 자동차에서 검사를 해줘야 된다!
(자동차 생성 시 이름의 길이를 검사해주는 방법을 사용할 수 있당)

class Car(val name: String, moveCount: Int = 0) {
    var moveCount = moveCount
        private set

    init {
        require(name.length <= MAX_NAME_LENGTH) { InputValidator.NAME_LENGTH_ERROR }
        require(name.isNotEmpty()) { InputValidator.INVALID_NAME_ERROR }
    }

자동차의 이름에 대한 제약사항이 도메인 모델에 포함되었기 때문에 입력 값에서 중복으로 검사할 필요가 없어지게 된다~
즉 InputValidator에서 자동차 이름의 길이를 검사하는 것은 불필요해지게 되는 것이다!

이는 일급 컬렉션이나 객체를 포장하는 방법등을 학습하면 조금더 이해가 될 것 같다는 리뷰어님의 말씀이 있었다😁

4. 함수 분리로 가독성 개선하기

배경

val maxCount = cars.maxOfOrNull { car ->
            car.getMoveCount()
        }
val winners = cars.filter { car ->
            car.getMoveCount() == maxCount
        }.map { winner ->
            winner.getName()
        }
outputView.printWinners(winners)

변수로 각 코드의 결과를 표현해줬지만 뭔가 가독성이 안좋아보인다.

변수로 만들어서 표현하는 것도 하나의 방법이 되겠지만 로직이 복잡하거나 해서 의미를 잘 전달하기 어렵다면 가장 쉬운 방법은 함수로 분리하는 것이다.

적용

위 코드와 아래 함수를 비교 해서 보면 훨씬 가독성이 좋아졌음이 느껴진다.

val maxCount = getMaxCount(cars)
val winners = findWinnersBy(cars, maxCount))
outputView.printWinners(winners)

참고

profile
공부 내용 기록

0개의 댓글