럿고의 개발 노트
전략 패턴 & 인스터페이 추상화 본문
전략패턴 & 인터페이스로 추상화
- 우아한테크코스 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