헤드 퍼스트 디자인 패턴을 읽고 정리한 글입니다.
전략 패턴 은 알고리즘군을 정의하고 각각의 알고리즘을 캡슐화하며 교환해서 사용할 수 있도록 만든다. 전략을 사용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.
오리 연못 시뮬레이션 게임에서 오리는 헤엄도 치고 꽥꽥거리는 소리도 내는 매우 다양한 오리 종류를 보여줄 수 있다. 가장 처음에는 표준적인 객체지향 기법을 사용하여 Duck이라는 수퍼클래스를 만든 다음, 그 클래스를 확장하여 다른 모든 종류의 오리를 생성
public abstract class Duck {
void quack() {
System.out.println("duck quack");
}
void swim() {
System.out.println("duck swim");
}
abstract void display();
}
class MallardDuck extends Duck {
@Override
void display() {
System.out.println("MallardD Duck");
}
}
class RedheadDuck extends Duck {
@Override
void display() {
System.out.println("Redhead Duck");
}
}
이제 오리들이 날아다닐 수 있도록 해야한다고 가정했을 경우 어떻게 해야할까??
public abstract class Duck {
void quack() {
System.out.println("duck quack");
}
void swim() {
System.out.println("duck swim");
}
abstract void display();
void fly() {
System.out.println("duck fly");
}
}
슈퍼클래스에 fly()메소드를 추가함으로써 모든 서브 클래스에서 fly()를 상속받도록 수정
class MallardDuck extends Duck {
@Override
void display() {
System.out.println("Mallard Duck");
}
}
class RedheadDuck extends Duck {
@Override
void display() {
System.out.println("Redhead Duck");
}
}
class RubberDuck extends Duck {
@Override
void quack() {
System.out.println("rubber duck quack");
}
@Override
void display() {
System.out.println("Rubber Duck");
}
}
하지만 해당 변경사항은 모든 오리가 날 수 있지 않다는 것을 간과했다. 고무로 된 오리 인형 클래스에도 비행 기능하게 된 것이다. 결론적으로 Duck 슈퍼클래스에 fly() 메소드가 추가되면서 일부 서브클래스에는 적합하지 않은 행동이 추가될 수 있는 사이드 이펙트가 발생하게 된 것이다.
RubberDuck의 quack() 메소드처럼 fly() 오버라이드를 해서 아무것도 하지 않게 한다면 가능은 하지만 어떤 부작용이 있을까??
이처럼 Duck 행동을 제공하는데 있어서 상속을 사용할 때의 단점은 많다.
그렇다면 상속 대신 Flyable, Quackable 인터페이스를 사용하는 방법은 어떨까??
서브클래스에서 Flyable, Quackable 인터페이스를 구현함으로써 고무오리의 비행기능 같은 문제점을 해결할 수 있지만, 코드 재사용성과 관리측면에서 더욱 큰 문제점이 발생하게 된다. (Java 8이하라고 가정)
디자인원칙1 달라지는 부분을 찾아내고, 달라지지 않는 부분으로부터 분리킨다.
결국 가장 중요한 것은 달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 캡슐화함으로써 나머지 부분에 영향을 미치지 않도록 한다면 시스템의 유연성을 향상시키는 것이다.
즉 확장에는 열려있고 수정에 대해서는 닫혀있도록 하는 것!!
모든 패턴은 시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는 방법을 제공하기 위한 것이다.
Duck 클래스에서 나는 행동과 꽥꽥거리는 행동을 추출하여 행동을 나타낼 클래스 집합을 새로 추가
디자인원칙2 구현이 아닌 인터페이스에 맞춰서 프로그래밍한다
각 행동들을 인터페이스 (FlyBehavior, QuackBehavior)로 표현하고 행동을 구현할 때 Duck 클래스에서 구현하느 것이 아닌 구체적인 행동 클래스에서 구현함으로써 특정 행동은 Duck 국한되지 않는다.
public interface FlyBehavior {
void fly();
}
public class FlyNoWay implements FlyBehavior{
@Override
public void fly() {
System.out.println("날 수 없다");
}
}
public class FlyWithWings implements FlyBehavior{
@Override
public void fly() {
System.out.println("나는 방법을 구현");
}
}
public interface QuackBehavior {
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("삑삑");
}
}
이로써 비행과 꽥꽥거리는 행동들은 Duck 클래스 안에 숨겨져 있지 않으므로 다른 객체에서도 이러한 행동들을 재사용할 수 있다. 그리고 기존의 Duck 클래스들을 전혀 수정하지 않고도 새로운 행동을 추가할 수 있다.
상속의 단점을 해결하는 것 뿐만 아니라 재사용성을 가질 수 있다.
이제 Duck 클래스는 행동을 다른 클래스에 위임함으로써 특정 행동을 할 수 있게 되었다.
public abstract class Duck {
private FlyBehavior flyBehavior;
private QuackBehavior quackBehavior;
public Duck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
this.flyBehavior = flyBehavior;
this.quackBehavior = quackBehavior;
}
public void performFly() {
flyBehavior.fly();
}
public void performQuack() {
quackBehavior.quack();
}
public abstract void display();
public void swim() {
System.out.println("모든 오리는 수영한다.");
}
}
class RubberDuck extends Duck {
public RubberDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
super(flyBehavior, quackBehavior);
}
@Override
public void display() {
System.out.println("Rubber Duck");
}
}
class MallardDuck extends Duck {
public MallardDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
super(flyBehavior, quackBehavior);
}
@Override
public void display() {
System.out.println("Mallard Duck");
}
}
class RedheadDuck extends Duck {
public RedheadDuck(FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
super(flyBehavior, quackBehavior);
}
@Override
public void display() {
System.out.println("Redhead Duck");
}
}
여태까지 사용했던 행동들을 일련의 행동이 아닌 알고리즘군으로 생각하고 똑같은 테크닉을 적용가능 하다.
ex) 지역별 세금 계산 방식 등등
각 오리에는 나는 행동과 꽥꽥하는 행동을 위임하기 위한 클래스들이 있다. 이처럼 두 클래스를 합치는 것을 구성(Composition)을 이용하는 것이라 한다. 오리 클래스에서는 행동을 상속받는 대신, 행동 객체로 구성됨으로써 행동을 부여받게 된다. 구성을 이용하면 유연성을 크게 향상시킬 수 있다. 캡슐화할 수 있도록 만들어주는 것 뿐만 아니라 실행시에 행동을 바꿀 수도 있게 해주기 때문이다.
디자인 원칙3: 상속보다는 구성을 활용한다.