전략 패턴 (Strategy patter)은
1.알고리즘군을 정의하고
2. 캡슐화해서
각각의 알고리즘군을 수정해서 쓸 수 있게 해줍니다.
전략 패턴을 사용하면 클라이언트로부터 알고리즘을 분리해서 독립적으로 변경 할 수 있습니다.
예를 들어, 다음과 같은 상황이 있다고 하자.
당신은 "DuckSimulator"의 개발자이다. 이 프로그램은 세상의 모든 오리의 행동을 구현해낸 유일한 프로그램이며, 세상의 모든 오리는 매우 다양하므로 당신은 Duck이라는 슈퍼클래스 아래에 모든 오리를 구현하겠다고 결심했다.
public abstract class FirstDuck {
public void fly(){
System.out.println("날고있어요!");
}
public void quack(){
System.out.println("꽥!");
}
public void swim(){
System.out.println("모든 오리는 물에 뜬다!");
}
public FirstDuck(){} // 생성자
public abstract void display(); // "내가 어떤 오리인가"를 보여주는 매서드
}
그런데, 오리 시뮬레이터에 이상한 일이 생겼다!
정말 기괴하지 않을 수 없었다. 따라서 당신은 황급히 매서드를 오버라이드 해서 재정의하기로 한다.
public class RubberDuck extends FirstDuck{
@Override
public void display() {
System.out.println("저는 고무오리입니다");
}
@Override
public void quack(){
System.out.println("삑!"); // 고무오리는 삑!
}
@Override
public void fly(){
System.out.println("<아무일도 일어나지 않았다>"); // 못날아요!
}
}
역시 당신은 슈퍼 개발자! 한번에 문제를 해결했다.
하지만, "모든" 오리가 존재하는 DuckSimulator 프로그램에서 RubberDuck과 같은 구현체는 너무너무 많았다.
당신은 그 모든 구현체 (20152개)에 @Override 해야하는 비극적인 상황을 마주하게 된 것이다!
이런 식의 반복은 오한이 들고 정신이 아득해질 것이다. 그렇다면 당신은 어떻게 이 부분을 해결해야 할까?
"오리" 슈퍼클래스가 가지는 알고리즘을 인터페이스로 먼저 정의하고, 그 알고리즘의 구현체를 적절히 캡슐화해서 사용 할 수 있지 않을까? 라는 생각에 이르게 된다.
public interface QuackBehavior {
public void quack();
}
public class Quack implements QuackBehavior{
@Override
public void quack() {
System.out.println("꽥!");
}
}
public class MuteQuack implements QuackBehavior{
@Override
public void quack() {
System.out.println("<아무일도 일어나지 않았다>");
}
}
public class Squeak implements QuackBehavior{
@Override
public void quack() {
System.out.println("삑!");
}
}
위와 같이 세 개의 적절한 구현체를 정의해서 사용이 가능하다!
public interface FlyBehavior {
public void fly();
}
public class CanFly implements FlyBehavior{
@Override
public void fly() {
System.out.println("날 수 있다말이야!");
}
}
public class CantFly implements FlyBehavior{
@Override
public void fly() {
System.out.println("못 난다 말이야!");
}
}
그렇다면, 우리의 오리들은 이 알고리즘들을 어떻게 사용 할 수 있을까?
FirstDuck은 이제 사라지고, "진짜" 슈퍼클래스인 Duck 클래스를 정의했다.
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){}
public abstract void display();
public void performFly(){
flyBehavior.fly();
// flyBehavior 에는 1. 난다, 2. 못난다 조정. (고무오리, 청동오리 ...)
}
public void performQuack(){
quackBehavior.quack();
// quackBehavior 는 1. 꽥! (청둥오리) 2. 삑! (고무오리==러버덕) 3. 무음 (나무오리)
}
public void swim(){
System.out.println("모든 오리는 물에 뜬다!");
}
이제 오리의 구현체를 손봐주러 가자
public class RealDuck extends Duck{
@Override
public void display() {
System.out.println("판사님 저는 진짜 오리입니다");
}
public RealDuck() {
quackBehavior = new Quack();
flyBehavior = new CanFly();
}
}
이 RealDuck 클래스는 생성자 매서드를 통해서 이 녀석이 스스로 어떤 알고리즘을 가지고 있는지를 정의할 수 있다.
따라서, 이 녀석의 모든 인스턴스는 이미 정의된 알고리즘의 구현체를 적절히 선택해서 태어난다
와!! 이러면 상속의 이점 + 코드의 중복을 줄일 수 있겠다.
"재미있는 이벤트를 준비했는데, 러버덕 같은 고무오리들도 날 수 있게 해보자! 사용자들이 좋아할 것 같은데?"
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
...
public void setFlyBehavior(FlyBehavior flyBehavior){
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior){
this.quackBehavior = quackBehavior;
}
}
위와 같이 알고리즘의 구현체를 선택 가능하게 만드는 setter 매서드를 Duck 슈퍼클래스에 내장해놓는 방법이었다!
때에 따라서 더 읽기 힘들 수 있다.
예를 들어, 오리의 flyable 인터페이스의 경우 if/else 구문으로 처리하는 게 더 가독성 측면에서는 유리할 것이다 (왜냐면, YES/NO 두 가지 알고리즘이니까)
즉, 전략 패턴을 어디에나 적용 할 수 있는 것은 아니다. 이 점이 매우 중요하다
모든 디자인 패턴은 상황에 따라 적절히 선택되어야 한다.
섹시균