럿고의 개발 노트

전략 패턴 & 인스터페이 추상화 본문

Java Note

전략 패턴 & 인스터페이 추상화

KimSeYun 2020. 3. 6. 13:09

전략패턴 & 인터페이스로 추상화

  • 우아한테크코스 2기에 참여하게 되면서 Level1 2번째 미션인 자동차 경주 게임을 진행하면서 배우게 된 새로운 디자인 패턴입니다.
  • 자동차 경주 게임은 랜덤값을 생성하여 랜덤값이 4이상이면 전진을 하는 상황이 존재합니다.
  • TDD를 이용하여 미션을 진행한 것이여서 랜덤값을 테스트 하기에는 어려웠습니다.
  • 테스트를 위해서는 자동차 전진이나 정지를 마음대로 조작할 수 있어야 하는데, 그것을 해결하는 방법이 인터페이스 추상화였습니다.
  • 처음에 구현할때는 테스트를 제대로 작성을 못했지만, 피드백을 받으면서 제대로된 자료를 찾게 되면서 구현하게 되었습니다.

예제

문제점

  • 처음에 구현했던 코드를 살펴보도록 하겠습니다.
public class Car {
    private String name;
    private int position;

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

    public int movePosition(int moveValue) {
        if (moveValue >= 4) {
            position++;
        }
        return position;
    }

    public int getPosition() {
        return position;
    }
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Cars {
    private List<Car> cars;

    public Cars(String[] names){
        this.cars = Arrays.stream(names)
                .map(Car::new)
                .collect(Collectors
                        .toList());
    }

    public int createRandomValue() {
        return (int)(Math.random() * 9);
    }

    public void moveCars(){
        for(Car car: cars){
            car.movePosition(createRandomValue());
        }
    }

    public List<Car> getCars() {
        return cars;
    }
}
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class CarTest {
    Car car = new Car("rutgo");

    @Test
    void 전진하기() {
        assertThat(car.movePosition(4)).isEqualTo(1);
    }

    @Test
    void 정지하기() {
        assertThat(car.movePosition(3)).isEqualTo(0);
    }
}
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class CarsTest {
    String[] names = {"pobi", "rutgo", "dlwlrma"};
    Cars cars = new Cars(names);

    @Test
    void 랜덤값_0에서_9까지_생성_테스트() {
        assertThat(cars.createRandomValue()).isBetween(0, 9);
    }
}
  • 일단, 랜덤값 자체를 컨트롤 했다기 보다는 movePostion의 인자인 moveValue를 컨트롤 해서 테스트를 하였다.
  • 또한, 여러 차가 움직이는 moveCars는 테스트 자체를 할 수가 없는 구조로 되어있다.
  • 위의 리뷰어 말처럼 테스트 하기 어려운 부분을 인터페이스로 추상화를 한다면 테스트를 하지 못한 코드와 테스트를 좀 더 의미있게 할 수 있을꺼 같다는 생각이 들었다.
  • 그렇다면 이제 인터페이스로 추상화를 해서 Random 값을 컨트롤 해보자.

해결법

public interface MoveValueStrategy {
    int createMoveValue();
}
  • interface를 하나 생성해서 움직이게 하는 값을 생성하는 메소드를 추상화 하자.
public class Application {
    public static void main(String[] args) {
        String[] names = {"pobi", "rutgo", "dlwlrma"};
        Cars cars = new Cars(names);
        MoveValueStrategy moveValueStrategy = () -> (int)(Math.random() * 9);
        cars.moveCars(moveValueStrategy);
    }
}
  • 인터페이스에 있는 createMoveValue에다가 원하는 값을 넣자.
  • 여기서는 Lambda를 사용했는데, DIP에 위반하고 있다고 볼 수도 있습니다.
  • 추상성이 높고 안정적인 고수준의 클래스는 구체적이고 불안정한 저수준의 클래스에 의존해서는 안된다 라는 원칙인데, 현재 람다로 구현한 부분은 저수준의 클래스에 의존하고 있다고 생각하면 좋을 것 같습니다.
  • 따라서 이런식으로 하는 것보다는 실제 객체를 구현하는 것이 훨씬 더 좋은 코드라고 할 수 있습니다.
public class Random implements MoveValueStrategy {
    @Override
    public int createMoveValue() {
        return (int) (Math.random() * 9);
    }
}
  • 위와 같이 Random이라는 Class를 만들어 주는 것이 안정적인 고수준의 클래스에 의존하고 있다고 볼 수 있을 것 같습니다.
package interfaceAbstractEx;

public class Application {
    public static void main(String[] args) {
        String[] names = {"pobi", "rutgo", "dlwlrma"};
        Cars cars = new Cars(names);
        Random random = new Random();
        cars.moveCars(random);
    }
}
  • 이런식으로 람다를 사용하는 것 보다는 객체를 직접 생성해서 객체를 넘겨주는 것이 훨씬 더 좋을 것 같습니다.
public class Car {
    private String name;
    private int position;

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

    public int movePosition(MoveValueStrategy moveValueStrategy) {
        if (moveValueStrategy.createMoveValue() >= 4) {
            position++;
        }
        return position;
    }

    public int getPosition() {
        return position;
    }
}
  • 또한 Car에서는 특정 값이 아닌 interface를 매개변수로 받아와서 값을 생성해주는 메소드를 사용하면 좋습니다.
  • 왜냐하면 전략패턴이라는 것은 여러개의 전략을 필요할때 사용하는 것이기 때문에, 내가 원하는 전략을 넣었을때 그 전략에 맞춰서 메소드는 실행을 해줘야 할 것입니다.
  • Random이라는 객체는 전략중에 하나일 뿐입니다. 따라서 Random을 받아 버린다면, 다른 전략들은 받을 수 가 없는 코드가 되버릴 것입니다.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Cars {
    private List<Car> cars;
    private MoveValueStrategy moveValueStrategy = () -> (int)(Math.random() * 9);

    public Cars(String[] names){
        this.cars = Arrays.stream(names)
                .map(Car::new)
                .collect(Collectors
                        .toList());
    }

    public void moveCars(MoveValueStrategy moveValueStrategy){
        for(Car car: cars){
            car.movePosition(moveValueStrategy);
        }
    }

    public List<Car> getCars() {
        return cars;
    }
}
  • moveCars메소드도 Car Class에서 설명한 내용과 동일합니다.
import interfaceAbstractEx.Car;
import interfaceAbstractEx.MoveValueStrategy;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class CarTest {
    Car car = new Car("rutgo");

    @Test
    void 전진하기() {
        MoveValueStrategy moveValueStrategy = () -> 4;
        assertThat(car.movePosition(moveValueStrategy)).isEqualTo(1);
    }

    @Test
    void 정지하기() {
        MoveValueStrategy moveValueStrategy = () -> 3;
        assertThat(car.movePosition(moveValueStrategy)).isEqualTo(0);
    }
}
  • 이런식으로 테스트를 할때 새로운 전략을 만들어서 테스트를 한다면 훨씬 더 테스트를 하기가 수월할 것 같습니다.
import interfaceAbstractEx.Cars;
import interfaceAbstractEx.MoveValueStrategy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class CarsTest {
    String[] names = {"pobi", "rutgo", "dlwlrma"};
    Cars cars = new Cars(names);

    @Test
    void 여러대의_차의_전진_확인() {
        MoveValueStrategy moveValueStrategy = () -> 4;
        cars.moveCars(moveValueStrategy);
        Assertions.assertThat(cars.getCars().get(0).getPosition()).isEqualTo(1);
        Assertions.assertThat(cars.getCars().get(1).getPosition()).isEqualTo(1);
        Assertions.assertThat(cars.getCars().get(2).getPosition()).isEqualTo(1);
    }
}
  • 이렇게 되었다면, 이제 여러대의 차가 전진하는 지를 확인을 할 수 있을 것 같습니다.

정리

  • 이런 식으로 전략을 이용한다면, 여러가지의 상황에 맞춰서 메소드를 컨트롤 할 수 있을 것입니다.

  • 참고자료
  • 위의 사진처럼 테스트가 어려운 코드는 맨 위로 빼서 테스트가 가능하도록 변경하는 것이 중요하다.
  • 또한 여러 가지 전략들이 생기게 된다면 인터페이스화를 해서 모든 전략을 한번에 관리 할 수 있도록 하는 것도 중요하다.

 

'Java Note' 카테고리의 다른 글

MVC Pattern 이란?  (0) 2020.02.17
Java API  (0) 2020.01.30
자바 속도 해결 방안(JIT 컴파일러, HotSpot)  (0) 2020.01.26
JVM, JDK, JRE  (0) 2020.01.25
자바(Java)란?  (0) 2020.01.23
Comments