[디자인패턴] Decorator Pattern
1. 카페의 음료를 나타내는 Beverage class 선언하기
☞ Beverage를 추상클래스로 선언한다면 모든 음료 메뉴에 대해 수많은 클래스가 생성되어야 함.
public class DecafWithWhip {
public float cost();
}
public class DarkRoastWithSoy {
public float cost();
}
public class HowseBlendWithMochandWhip {
public float cost();
}
// ....
2. 수퍼클래스 및 인스턴스 변수 사용으로 변환
- Beverage SuperClass
public class Beverage {
public float milkCost;
public float soyCost;
public float mochaCost;
// getter, setter
public float cost() {
float condimentCost = (float) 0.0;
if(hasMilk()) {
condimentCost += milkCost;
}
if(hasSoy()) {
condimentCost += soyCost;
}
if(hasMocha()) {
condimentCost += mochaCost;
}
return condimentCost;
}
}
- DarkRoast SubClass
public class DarkRoast extends Beverage {
public DarkRoast() {
description = "Dark Roast Coffee";
}
public float cost() {
return (float)(1.99 * super.cost());
}
}
☞ 첨가물 가격이 바뀔 때마다 코드를 수정해야 한다.
- 첨가물의 종류가 많아지면 새로운 메소드를 추가해야 하고, SuperClass의 cost() 메소드도 수정해야 한다.
- 특정 첨가물이 들어가면 안되는 음료의 경우에도 SuperClass의 hasXXX() 메소드를 상속받게 된다.
- 손님이 특정 첨가물을 여러 번 주문할 경우는 어떻게 해야 할까???
서브 클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일시에 완전히 결정될 뿐만 아니라 모든 서브 클래스에서 똑같은 행동을 상속받아야 한다.
☞ 구성을 통해서 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있다.
# 디자인 원칙 4
OCP(Open-Closed Principle)
클래스는 확장에 대해서는 열려 있어야 하지만, 코드 변경에 대해서는 닫혀 있어야 한다.
3. Decorator Pattern 개요
ex) 모카와 휘핑크림을 추가한 다크로스트 커피
① DarkRoast 객체를 가져온다.
DarkRoast는 Beverage를 상속받기 때문에 음료의 가격을 계산하는 cost() 메소드를 가짐
② Mocha 객체로 장식(decorate)한다.
Mocha 객체는 Decorator이다.
Decorator의 형식은 Decorator 객체가 장식하고 있는 객체를 반영한다.
따라서, Mocha에도 cost() 메소드가 있고, 다형성을 통해서 Mocha가 감싸고 있는 것도 Beverage 객체로 간주할 수 있다.
(Mocha도 Beverage의 서브 클래스 형식)
③ Whip 객체로 장식(decorate)한다.
Whip 객체도 Decorator이다.
Mocha와 Whip으로 싸여 있는 DarkRoast는 여전히 Beverage 객체이기 때문에 cost() 메소드를 비롯한 DarkRoast에 대해 할 수 있는 것은 뭐든 할 수 있다.
④ cost() 메소드를 호출한다. 이 때, 첨가물의 가격을 계산하는 일은 해당 객체들에게 위임된다.
4. Decorator Pattern 특징
- Decorator의 SuperClass는 자신이 장식하고 있는 객체의 SuperClass와 같다.
- 한 객체를 여러 개의 Decorator로 감쌀 수 있다.
- Decorator는 자신이 감싸고 있는 객체와 같은 SuperClass를 가지고 있기 때문에 원래 객체(싸여져 있는 객체)가 들어갈 자리에 Decorator 객체를 집어넣어도 상관없다.
- Decorator는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 외에 원하는 추가적인 작업을 수행할 수 있다.
- 객체는 언제든지 감쌀 수 있기 때문에 실행 중에 필요한 Decorator를 마음대로 적용할 수 있다.
Component(구성요소), ConcreteComponent
- 각 구성요소는 직접 쓰일 수도 있고, Decorator로 감싸져서 쓰일 수도 있다.
- ConcreteComponent에는 Component가 가지는 행동 외에 새로운 행동을 동적으로 추가할 수 있다.
Decorator(첨가물), ConcreteDecorator
- 각 Decorator에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수가 있다.
- Decorator는 자신이 장식할 구성요소와 같은 인터페이스 또는 추상 클래스를 구현한다.
- Decorator는 Component의 상태를 확장할 수 있고, 새로운 메소드를 추가할 수 있다.
하지만 일반적으로 새로운 메소드를 호출하기 전, 또는 후에 별도의 작업을 처리하는 방식으로 새로운 기능을 추가한다.
객체에 추가적인 요건을 동적으로 첨가한다.
SubClass를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.
5. 코드 예시
- Beverage class (Component 구성요소)
public abstract class Beverage {
String description = "제목 없음";
String getDescription() {
return description;
}
public abstract double cost();
}
- Decorator class (Condiment 첨가물)
Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하므로 Beverage 클래스를 확장한다.
모든 첨가물 Decorator에서 getDescription() 메소드를 새로 구현하도록 하기 위해 추상 메소드로 선언한다.
public abstract class CondimentDecorator extends Beverage {
public abstract String getDescription();
}
- 음료 class (ConcreteComponent, Beverage 구체화)
public class Espresso extends Beverage {
public Espresso() {
description = "에스프레소";
}
public double cost() {
reutrn 1.99;
}
}
public class HouseBlend extends Beverage {
public HouseBlend() {
description = "하우스 블렌드 커피";
}
public double cost() {
return .89;
}
}
public class DarkRoast extends Beverage {
public HouseBlend() {
description = "다크 로스트 커피";
}
public double cost() {
return .99;
}
}
public class Decaf extends Beverage {
public HouseBlend() {
description = "디카페인 커피";
}
public double cost() {
return 1.05;
}
}
- 첨가물 class (ConcreteDecorator, CondimentDecorator구체화)
Mocha는 Decorator이기 때문에 CondimentDecorator를 확장하고, CondimentDecorator는 Beverage를 확장한다.
Mocha 인스턴스는 Beverage의 인스턴스 변수를 가진다.
public class Mocha extends CondimentDecorator {
Beverage beverage;
public Mocha(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", 모카";
}
public double cost() {
return .20 + beverage.cost();
}
}
public class Milk extends CondimentDecorator {
Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", 스팀 우유";
}
public double cost() {
return .10 + beverage.cost();
}
}
public class Soy extends CondimentDecorator {
Beverage beverage;
public Soy(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", 두유";
}
public double cost() {
return .15 + beverage.cost();
}
}
public class Whip extends CondimentDecorator {
Beverage beverage;
public Whip(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", 휘핑크림";
}
public double cost() {
return .10 + beverage.cost();
}
}
- main class
public class StarBuzzCoffee {
public static void main(String[] args) {
Beverage beverage = new Espresso();
System.out.println(beverage.getDescription() + " $" + beverage.cost());
// 에스프레소 $1.99
Beverage beverage2 = new DarkRoast();
beverage2 = new Mocha(beverage2);
beverage2 = new Mocha(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescription() + " $" + beverage2.cost());
// 다크 로스트 커피, 모카, 모카, 휘핑크림 $1.49
Beverage beverage3 = new DarkRoast();
beverage3 = new Soy(beverage3);
beverage3 = new Mocha(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription() + " $" + beverage3.cost());
// 하우스 블렌드 커피, 두유, 모카, 휘핑크림 $1.34
}
}
6. Decorator Pattern의 한계
- 특정 구상 구성요소인지 확인한 다음 어떤 작업을 처리하는 (예를 들어, 하우스 블렌드 커피를 특별 할인하는 경우 등) 경우에는 Decorator Pattern을 사용할 수 없다.
구성요소를 Decorator로 감싸게 되면 구성요소가 무엇인지 알 수 없기 때문이다.
추상 구성요소 형식을 바탕으로 돌아가는 코드에 대해서 Decorator Pattern을 적용해야만 제대로 된 결과를 얻을 수 있다. - Decorator Pattern을 이용하면 관리해야 할 객체가 늘어나고, 그에 따라 코딩하는 과정에서 실수를 할 가능성이 높아지게 된다. 하지만, Decorator는 일반적으로 Factory Builder 같은 다른 패턴을 써서 만들고 사용하기 때문에 Decorator로 장식된 구상 구성요소를 만드는 것이 잘 캡슐화 되어있다는 것을 알 수 있다.
7. Decorator Pattern이 적용된 예 : 자바 I/O
자바 I/O 패키지의 많은 부분은 Decorator Pattern을 바탕으로 만들어졌다.
...