전략패턴
예)
상황에 따라 다른 가격 할인 정책
- 첫손님 할인
- 저녁 시간대 할인
계산하는 모듈에 가격 할인 정책
public class Calculator {
public int calculate(boolean firstGuest, List<Item> items) {
int sum = 0;
for(Item item : items) {
if(firstGuest)
sum += (int)(item.getPrice() * 0.9 ) ; //첫손님
else if (! item.isFresh())
sum += (int)(item.getPrice() * 0.8 ) ; // 덜신선한 상품
else
sum += item.getPrice();
}
return sum;
}
}
문제점)
1. 서로 다른 계산 정책들이 한 코드에 섞여 있다.
2. 추가될수록 코드 분석이 어럽게 만든다.
3. 가격 정책이 추가될때 마다 calculate 메서드를 수정하는 것이 점점 어려워진다.
-마지막 손님 50% 할인과 같은 새로운 가격 정책이 추가될경우, 메서드 안에 마지막 손님을 구분하기 위한 lastGuest 파라미터가 추가되고 if 블록이 더 추가된다.
가격할인 정책을 별도 객체로 분리작업
DiscountStrategy 인터페이스는 상품의 할인금액 계산을 추상화하고, 각 콘크리트 클래스는 상황에 맞는 할인 계산 알고리즘을 제공한다.
Calculator 클래스는 가격 합산 계산의 책임을 진다.
가격할인 알고리즘(계산방법)을 추상화하고 있는 DiscountStrategy를 전략(Strategy)이라고 부르고 가격 계산 기능 자체의 책임을 갖고 있는 Calculator를 콘텍스트(Context)라고 부르는데, 이렇게 특정 콘텍스트에서 알고리즘(전략)을 별도로 분리하는 설계방법이 전략 패턴이다.
전략 패턴에서 콘텍스트는 사용할 전략을 직접 선택하지 않는다.
대신, 콘텍스트의 클라이언트가 콘텍스트에 사용할 전략을 전달해준다.
즉, DI를 이용해서 콘텍스트에 전략을 전달해준다. 그리고 전략이 어떤 메서드를 제공할지의 여부는 콘텍스트가 전략을 어떤 식으로 사용하느냐에 따라 달라진다.
DiscountStrategy로 분리한경우,
public class Calculator {
private DiscountStrategy discountStrategy;
public Calculator(DiscountStrategy discountStrategy) {
this.discountStrategy = discountStrategy;
}
public int calculate(List<Item> items) {
int sum = 0;
for(Item item : items) {
sum += discountStrategy.getDiscountPrice(item);
}
return sum;
}
}
Calculator 클래스는 생성자를 통해서 사용할 전략 객체를 전달받고, calculate() 메서드에서 각 Item의 가격을 계산할때 전략 객체를 사용하고 있다.
Calculator는 각 Item 별로 할인 정책을 적용하고 있다.
public interface DiscountStrategy {
int getDiscountPrice(Item item);
}
각 아이템별로 할인정책이 있고 전체 금액에 대한 할인 정책이 별도로 필요하다면
DiscountStrategy의 인터페이스에 전체 금액 할인을 위한 메서드가 추가되어야 한다.
public interface DiscountStrategy {
int getDiscountPrice(Item item);
int getDiscountPrice(int totalPrice);
}
아니면, 전체 금액할인정책을 위한 전략을 별도 인터페이스로 분리할 수 있다.
public interface ItemDiscountStrategy {
int getDiscountPrice(Item item)
}
public interface TotalDisCountStrategy{
int getDiscountPrice(int totalPrice);
}
전략 객체는 콘텍스트를 사용하는 클라이언트에서 직접 생성한다.
첫번째 손님에 대한 할인을 해주는 firstGuestDiscountStrategy 구현클래스
public class FirstGuestDiscoutStrategy implements DiscountStrategy {
@Override
public int getDiscountPrice(Item item) {
return (int) (item.getPrice() * 0.9);
}
}
첫번째 손님일 경우 계산을 하면 계산에서 첫번째 손님 할인 적용 버튼을 누른 뒤에 계산 버튼을 누를 것이다.
private DiscountStrategy strategy;
public void onFirstGuestButtonClick() {
//첫번째 손님 할인 버튼 누를 때 생성됨
strategy = new FirstGuestDiscoutStrategy();
}
public void onCalculationButtonClick() {
//계산 버튼 누를 때 실행
Calculator cal = new Calculator(strategy);
int pirce = cal.calculate(items);
}
Calculator를 사용하는 코드에서 FirstGuestDiscountStrategy 클래스의 객체를 생성한다.
콘텍스트를 사용하는 클라이언트가 전략의 상세 구현에 대한 의존이 발생한다.
콘텍스트의 클라이언트가 전략의 인터페이스가 아닌 상세 구현을 안다는 것이 문제처럼 보일 수 있으나, 이 경우에는 전략의 콘크리트 클래스와 클라이언트 코드가 쌍을 이루기 때문에 유지보수 문제가 발생할 가능성이 줄어든다.
할인정책을 추가하려면 클라이언트에 할인정책 버튼을 클릭하는 코드가 생기고, 이 코드에서 NoFreshItemDiscountStrategy 객체를 생성해 주게 된다.
또한, 기능이 제거될 때에도 함께 제거 된다.
따라서 클라이언트의 버튼 처리 코드에서 전략 객체를 직접 생성하는 것은 오히려 코드 이해를 높이고 코드 응집을 높여주는 효과를 갖는다.
전략패턴을 적용할 때 얻을 수 있는 이점은 콘텍스트 코드의 변경 없이 새로운 전략을 추가할 수 있다는 점이다.
마지막 속님 대폭 할인 정책을 추가하는 경우, 계산를 제공하는 Calcurator 클래스의 코드를 변경하지 않는다.
단지 새로운 할인 정책을 구현한 LastGuestDiscountStrategy 클래스를 추가하고, 마지막 송님 대폭 할인 버튼 클릭을 처리하는 코드에서 LastGeustDiscountStrategy의 객체를 생성해 주기만 하면 된다.
private DiscountStrategy strategy;
public void onLastGuestButtonClick() {
strategy = new LastGuestDiscountStrategy;
}
public void onCalculationButtonClick {
Calculator cal = new Calculator(strategy);
int price = cal.calculate(items);
}
전략 패턴을 적용함으로써 Calculator 클래스는 할인 정책 확장에는 열려 있고 변경에는 닫혀 있게된다.
즉, 개방폐쇄원칙을 따르는 구조를 갖게된다.
if-else으로 구성된 코드 블록이 비슷한 기능(비슷한 알고리즘)을 수행하는 경우에 전략 패턴을 적용함으로써 코드를 확장 가능하도록 변경할 수 있다.
calculate() 메서드의 if-else 블록에 전략패턴을 적용함으로써 새로운 할인 정책을 보다 쉽게 추가할 수 있도록 만들었다.
완전히 동일한 기능을 제공하지만 성능의 장단점에 따라 알고리즘을 선택해야 하는 경우에도 전략패턴을 사용한다.
XML을 파싱해서 객체를 생성하는 기능을 사용해야한다면,
XML을 파싱하는 알고리즘을 Unmarshaller 타입으로 분리하고 성능 요구에 따라 DOM이나 SAX를 사용하는 콘크리트 Unmarshaller를 선택하도록 구현할 수 있다.
'JAVA > Design Patterns' 카테고리의 다른 글
데코레이터 패턴 (0) | 2018.11.27 |
---|---|
상태패턴 (0) | 2018.11.27 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제 요약 (0) | 2018.02.19 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제3 (0) | 2018.02.14 |
인터페이스로 프로그래밍하기 #깨지기 쉬운 기반 클래스 문제2 (0) | 2018.02.14 |