[Design Pattern] Decorator Pattern
# 개요
이미 존재하는 객체를 dynamic wrapping하여 책임과 행동을 추가하는 패턴
- wrapping한 객체에 새로운 기능을 추가
# 문제 상황
아래는 커피숍에 대한 예시이다.
만약 새로운 첨가물을 추가하고 싶다면 아래와 같이 할 수 있다.
그러나, 클래스가 너무 많다.
물론 attribute로 표현하면 클래스가 늘어나는 것은 막을 수 있다.
그러나 Beverage는 상위 클래스이므로 변경이 잘 없어야 하지만
아래와 같이 변경이 일어날 때 마다 cost함수 변경이 필요하다. (OCP 위반)
public class Beverage {
protected String description;
boolean milk, soy, mocha, whip;
public float cost () {
float condimentCost = 0.0;
if (hasMilk())
condimentCost += milkCost;
if (hasSoy())
condimentCost += soyCost;
if (hasMocha())
condimentCost += mochaCost;
if (hasWhip())
condimentCost += whipCost;
return condimentCost;
}
}
OCP를 적용하여 해결할 수도 있는데, 너무 과도하게 적용하면 좋지 않다는 점에 유의해야 한다.
# 적용 예시
위 예시를 다음과 같이 해결한다.
객체를 wrapping하고 연쇄적으로 자신의 안에 있는 객체에게 필요한 정보를 물어보면서 계산한다.
클래스 다이어그램으로 일반화하면 다음과 같다.
Decorator를 중심으로 ConcreteDecorator들이 추가된다.
여기서 중요한 점이 두 가지 있다.
1. Decorator와 Component는 1:1 관계이다. (Association)
2. Decorator는 Component를 상속 받는다. (Inheritance)
Association과 Inheritance에 대한 화살표가 모두 다 있어야 함에 유의하자.
위의 커피 예제에 이 방식을 적용하면 다음과 같다.
코드로 표현하면 다음과 같다.
public abstract class Beverage {
protected String description = “Unknown Beverage”;
public String getDescription() {
return description;
}
public abstract double cost();
}
public class Espresso extends Beverage {
public Espresso() {
description = “Espresso”;
}
public double cost() {
return 1.99;
}
}
Beverage 클래스의 cost 메서드는 하위 클래스에서 구현할 수 있도록 abstract method로 구현되어 있다.
Extends로 받으면 상위 객체의 메서드, 변수를 그대로 사용할 수 있다.
public abstract class CondimentDecorator extends Beverage {
protected Beverage beverage;
public abstract String getDescription();
}
public class Mocha extends CondimentDecorator {
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + “, Mocha”;
}
public double cost() {
return .20 + beverage.cost();
}
}
Decorator의 생성자에서는 beverage 인스턴스가 들어오면 자기 자신의 것으로 갖는다.
cost는 자신의 cost와 꾸미고 있는 객체의 cost를 더해서 계산한다.
테스트 코드는 다음과 같다.
public class StarbuzzCoffee {
public static void main(String args[]) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + “ $” + beverage.cost());
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + “ $” + beverage2.cost());
Beverage beverage3 = new HouseBlend();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + “ $” + beverage3.cost());
}
}
인스턴스를 만들어 줄 때 마다 cost가 추가된다. (런타임에 실행)
## 상속을 잊지 말자
위에서 정리했듯 Association과 더불어 Inheritance가 꼭 필요하다.
아래 그림을 보면 상속 관계에 의해 Component에서 정의한 methodA, methodB를 Concrete Component와 Concrete Decorator들에서 링크할 수 있다.
Decorator Pattern에서는 wrapping이 연쇄적으로 일어나므로 Concrete Component를 Concrete Decorator 1, 2가 연쇄적으로 wrapping 할 수 있는데 상속이 없으면 Concrete Decorator2가 Concrete Decorator 1을 가르킬 수 없다.
상속이 있어야 Component 타입을 가르킬 수 있으므로 hierarchy를 만들 수 있다.
# 관련된 패턴
인터페이스와 관련된 패턴들과 비교해 볼 수 있다.
Adapter Pattern: 서로 상이한 인터페이스를 맞춰주는 패턴. (different interface)
Proxy Pattern: 실제 사용할 subject와 같은 인터페이스를 proxy가 제공한다. (same interface)
Decorator Pattern: 기존의 객체에 새로운 책임을 부여한다. (enhanced interface)
# 요약
- Open-Closed Principle (OCP)를 만족하는 패턴
- 새로운 책임을 부여할 수 있고 하위 클래스가 너무 많아지는 것을 막을 수 있다.
- Composition & Delegation
- Decorator Class는 꾸미고 있는 컴포넌트와와 동일한 타입의 부모를 상속받아서 연쇄적으로 wrap할 수 있다.
- 자잘한 클래스를 여러 개 만들고 해당 패턴을 모르는 사람은 이해하기 어려운 단점이 있다.