야돈은 야돈으로써의 기능과, 야도란으로 진화하면서 얻는 기능을 모두 수행 가능하다.
즉, 야돈은 야돈이되 "확장된" 야돈으로 기능 가능하다! (?)
데코레이터 패턴(Decorator Pattern)은
객체에 추가 요소를 동적으로 더할 수 있다.
데코레이터를 사용하면, 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장 할 수 있다.
스타버즈 코리아는 떠오르는 혜성처럼 등장한 커피 가게이다. 해당 회사는 고객이 원하는 토핑(휘핑, 모카 등)을 금액을 지불하면 맘껏 추가할 수 있도록 하는 컨셉으로 큰 인기를 얻었으며, 규모가 커진 이제는 "주문 시스템"을 만들려고 한다.
스타버즈는 이에 따라 슈퍼 개발자인 당신에게 "주문 시스템"의 개선을 거액에 의뢰하게 되는데...
"음료"의 추상 클래스인 Beverage 를 활용하는 형식이다.
추상 매서드인 cost() 를 활용해서 각각의 가격을 리턴하고,
getDescription 매서드를 오버라이드 해서 각자의 설명을 해주고 있는 식이었다.
그런데, 사업을 전개하면서 문제가 발생했다. 당신도 이미 스타버즈 커피를 많이 마셔봤겠지만, 음료에 다양한 커스텀이 가능하게 된 것이다!
그렇다면 이제 Beverage 클래스의 구현체로 "에스프레스 샷 두번추가" 클래스, "에스프레소 샷 세번 추가하고 모카 두번추가하고 휘핑 두번 추가할까말까" 클래스
...
엄청난 클래스들이 생겨야 한다!! 이걸 어떻게 해결해야 할까?
슈퍼 개발자인 당신은 일단 책임의 분리를 명확히 하고자 했다.
한 눈에 봐도, Espresso 등 "커피"의 시작과, 휘핑 등 "추가하는 것"은 다르다.
public abstract class Beverage {
String description = "음료류!";
public String getDescription(){
return this.description; // getter 매서드는 구현체들도 공유하므로, 선언한다.
}
public abstract double cost(); // Beverage의 구현체들은 가격 매서드를 구현해야 한다.
}
public abstract class Decorator {
public abstract String getDescription();
// 모든 첨가물에도 Description 을 새로 구현하도록 만들것이다.
}
public Mocha(Beverage beverage){
double cost = beverage.cost();
cost += 0.22;
String description = beverage.getDescription();
description += ", 모카";
}
잠시 쉬운 방법의 유혹에 흔들렸지만, 당신은 이내 정신을 차리고 이 방법의 단점을 알아냈다.
(글을 쓰는 나는 왜?를 생각해내기까지 30분이나 걸렸다...)
일단, Beverage 가 더이상 Beverage가 아니게 된다. 그리고 이것은 객체를 운영 할 때 매우 문제가 된다.
그렇다면 어떻게 하나요?
당신은 번뜩이는 아이디어를 가지고, Decorater 추상을 다음과 같이 바꾸게 된다.
이제 실제 코드를 알아보자
public abstract class Beverage {
String description = "음료류!";
public String getDescription(){
return this.description; // getter 매서드는 구현체들도 공유하므로, 선언한다.
}
public abstract double cost(); // Beverage의 구현체들은 가격 매서드를 구현해야 한다.
}
public abstract class Decorator extends Beverage {
Beverage beverage; // 데코레이터가 감쌀 음료 객체를 정한다
public abstract String getDescription();
// 모든 첨가물에도 Description 을 새로 구현하도록 만들것이다.
}
public class Mocha extends Decorator{
// Decorator 는 Beverage(Component) 를 확장한다.
public Mocha(Beverage previousBeverage){
this.beverage = previousBeverage;
// 생성자 매서드를 통해서 매개변수로 들어온 previousBeverage 가 필드의 인스턴스 변수가 된다
// Decorator 클래스의 인스턴스 변수로 선언한 beverage 가 된다.
}
@Override
public double cost() {
return beverage.cost() + 0.2;
}
@Override
public String getDescription() {
return beverage.getDescription() + ", 모카";
// getDescription 매서드를 통해 이전에 있던 음료
// (혹은 데코레이터) 설명 호출 후 덧붙이기
}
}
위와 같이, Component -> Decorator -> 구현체 순서로 확장된다
Mocha 라는 데코레이터의 생성자 매서드는 매개변수로 previousBeverage 를 가지는데, 이렇게 들어온 녀석은 "모카"필드의 인스턴스 변수인
Beverage beverage;
가 된다.
그리고 이 녀석을 가공도 할 수 있다.
component인 Beverage 만이 가지던 cost() 매서드를 오버라이드 해서, 생성 할 때 들어온 객체에 가공이 가능하다!
그러면, 어떻게 가공이 되는지 한번 보자!
Beverage 추상은 계속 유지된다.
객체가 계속 가공되어도 Component 에서 벗어나지 않는다.
그래서 뭐!!? 정리해서 말해봐!
데코레이터의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스이다.
한 객체를 여러 개의 데코레이터로 감쌀 수 있다.
데코레이터는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기 때문에, 원래 객체(싸여있는 객체)가 들어갈 자리에 데코레이터 객체를 넣어도 상관이 없다
(휘핑에 휘핑에 휘핑에 휘핑)...
데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행 할 수 있다.
ex) Java.io -> BufferedReader / InputStream ... 추가 작업의 마뜨료시카 녀석들
OCP
: 각 데코레이터를 통해서 Component 를 유연하게 확장 가능하다.
다형성 활용
: 데코레이터는 기존의 Component 객체를 상속받으므로, 데코레이터를 사용하는 클라이언트는 원본 객체 또는 데코레이터 객체를 모두 사용할 수 있다.