-
[Design Pattern] Decorator PatternLearn/Architecture 2022. 9. 24. 17:07
# 개요
이미 존재하는 객체를 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할 수 있다.
- 자잘한 클래스를 여러 개 만들고 해당 패턴을 모르는 사람은 이해하기 어려운 단점이 있다.
'Learn > Architecture' 카테고리의 다른 글
[Design Pattern] Bridge Pattern (0) 2022.09.25 [Design Pattern] Composite Pattern (0) 2022.09.25 [Design Pattern] Adapter Pattern (0) 2022.09.04 [Design Pattern] Singleton Pattern (0) 2022.08.31 [Design Pattern] Builder Pattern (0) 2022.08.30