다양한 기능을 조합시킬수 있다
자바의 입출력 스트림은 decorator pattern
다른 데코레이터나 또는 컴포넌트를 포함해야 함
Decorator Pattern을 활용하여 커피를 만들어 봅시다.
아메리카노
카페 라떼 = 아메리카노 + 우유
모카 커피 = 아메리카노 + 우유 + 모카시럽
크림 올라간 모카커피 = 아메리카노 + 우유 + 모카시럽 + whipping cream
커피는 컴포넌트고, 우유, 모카시럽, whipping cream은 모두 데코레이터임
public abstract class Coffee{
public abstract void brewing();
}
// 구현은 다했지만 상속을 위해 추상 클래스로 구현
public abstract class Decorator extends Coffee{
Coffee coffee;
public Decorator(Coffee coffee){
this.coffee = coffee;
}
@Override
public void brewing(){
coffee.brewing();
}
}
// 커피를 상속받은 데코레이터를 상속받았기 때문에 커피 가지고 있는 것
public class Latte extends Decorator{
public Latte(Coffee coffee){
super(coffee);
}
public void brewing(){
super.brewing();
System.out.print("Adding Milk");
}
}
public class Mocha extends Decorator{
public Mocha(Coffee coffee){
super(coffee);
}
public void brewing(){
super.brewing();
System.out.print("Adding Mocha Syrup");
}
}
public class WhippedCream extends Decorator{
public WhippedCream(Coffee coffee){
super(coffee);
}
public void brewing(){
super.brewing();
System.out.print("Adding WhippedCream");
}
}
public class KenyaAmericano extends Coffee{
@Override
public void brewing(){
System.out.print("Kenyaamericano");
}
}
public class EtiopiaAmericano extends Coffee{
@Override
public void brewing() {
System.out.print("EtiopiaAmericano ");
}
}
public class CoffeeTest {
public static void main(String[] args) {
Coffee kenyaAmericano = new KenyaAmericano();
kenyaAmericano.brewing();
System.out.println();
Coffee kenyaLatte = new Latte(kenyaAmericano);
kenyaLatte.brewing();
System.out.println();
Mocha kenyaMocha = new Mocha(new Latte(new KenyaAmericano()));
kenyaMocha.brewing();
System.out.println();
WhippedCream etiopiaWhippedMocha =
new WhippedCream(new Mocha(new Latte( new EtiopiaAmericano())));
etiopiaWhippedMocha.brewing();
System.out.println();
}
}
출처 : https://velog.io/@hanna2100/디자인패턴-3.-데코레이터-패턴-개념과-예제-decorator-pattern
스타벅스 사이렌 오더처럼 원하는 음료를 커스텀(샷추가, 휘핑변경 등)하여 주문할 수 있는 앱을 만든다고 합시다.
abstract class Beverage {
description // "녹차 프라푸치노"같은 음료 설명 변수
getDescription()
cost() // 추상메소드. 모든 음료는 추상클래스 Beverage를 상속받아 구현.
}
class Americano extends Beverage {
...
cost()
}
class CafeLatte extends Beverage {
...
cost()
}
음료를 정의하는 Beverage추상클래스를 만들었습니다. 모든 음료는 이 추상클래스를 상속받아 cost, description 등을 구체화합니다.
이 방식은 엄청나게 많은 서브클래스를 만든다는 단점이 있습니다. '휘핑크림'같은 옵션이 추가될 때마다 AmericanoWithWhippingCream, CafeLatteWithWhippingCream 등 의 클래스들이 음료 종류별로 생겨나겠죠.
또한 휘핑가격이 인상되기라도 한다면 휘핑이 있는 모든 클래스의 cost()를 수정해야합니다. 즉, 이런 방법은 매우 비효율적이죠.
이 방법은 어떤가요?
abstract class Beverage {
description
milk
soy
whip
getDescription()
hasMilk()
setMilk()
hasSoy()
setSoy()
hasWhip()
setWhip()
cost() // 추가된 옵션항목(milk, soy, whip)들을 계산함.
}
class Americano extends Beverage {
...
cost() // 아메리카노 음료 가격에서 수퍼클래스의 cost()를 호출하여 추가요금을 더해줌
}
수퍼클래스에서 옵션가격을 계산해주므로 더이상 ~WithWhippingCream과 같은 서브클래스를 만들지 않아도 됩니다. 하지만 여전히 문제는 남아있습니다.
디자인원칙
클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.
예를 들어, Beverage 클래스를 확장하여 BubbleTea
클래스를 새로 만드는 건 자유입니다. 하지만 BubbleTea에 펄추가옵션
이 필요하다고 해서, Beverage 클래스에 펄과 관련된 메소드들을 맘대로 추가하면, 이 코드로 인해 Beverage를 상속받는 다른 클래스에 버그가 생길수 도 있습니다. 즉 기존의 코드는 최대한 변경되어선 안됩니다
. 이것을 OCP 원칙
이라고 합니다.
물론 무조건 OCP를 적용하는 것이 쓸데없는 시간낭비가 될 수 도 있습니다. 결과적으로 불필요하고, 복잡하고, 이해하기 힘든 코드만 남을 수도 있습니다. 그렇기 때문에 바뀌는 부분 중에서 중요한 부분을 선별할 수 있는 객체지향적 안목이 필요합니다.
Decorator 란 장식가를 뜻합니다. 무언갈 꾸며주는 역할을 하지요. 데코레이터 패턴은 객체를 래핑
하는 방식으로 다른 객체를 꾸밀 수 있습니다.
프라푸치노 인스턴스를 생성한다.
녹차 객체로 장식한다
휘핑 객체로 장식한다
cost() 메소드를 호출한다. 이때 첨가물 가격을 계산하는 일은 객체들에게 위임된다.
데코레이터 패턴으로 만든 휘핑추가한 녹차 프라푸치노
입니다. 여기서 녹차와 휘핑의 객체형식은 프라푸치노와 마찬가지로 Beverage 타입
입니다. 따라서 모두가 cost()메소드
를 가지고 있습니다.
위 그림에서 처럼, 래핑된 객체에게 cost()메소드를 호출
하면 가장 안쪽의 수퍼객체까지 일이 위임
됩니다. 가장 안쪽의 수퍼객체는 자신의 cost()메소드를 리턴하고, 그 값을 받은 래핑객체(녹차, 휘핑)는 위에서 받은 값에 자신의 가격을 추가
하여 cost()값을 리턴
합니다.
데코레이터패턴에서는 객체에 추가적인 요건을 동적으로 추가할 수 있습니다. 또한 서브클래스를 통해 기능을 확장할 수 도 있습니다.
위 클래스 다이어그램은 데코레이터 패턴을 적용시킨 큰 그림입니다. 이 패턴에 맞춰 커피주문앱의 클래스 다이어그램을 생각해봅시다.
(여기서 소개하는 단어들은 Head First Design Patterns 책에서 영어로 된 클래스명을 한글로 번역한겁니다)
1. 구성
커피주문앱에서 Beverage 클래스가 구성에 해당됩니다. 이 구성클래스는 추상클래스일 수도 , 인터페이스일 수도 있습니다.
2. 행동구성(구체화된 구성)
커피주문앱에서 Frappuccino, Americano등이 해당됩니다. 구성요소를 나타내는 구체적인 구성클래스입니다.
3. 데코레이터
Beverage 클래스를 확장하여 BeverageDecorator
클래스를 만듭니다. 같은 상속이지만 이전의 Bad Case와 다른 점은 상속을 통해서 형식만 맞추는 것이지, 구체적인 행동을 물려받진 않습니다
. 구체적인 행동은 행동구성클래스들에 정의될 테니까요
. 이 행동구성클래스들에게서 필요한 행동만 가져와서 데코레이터패턴을 꾸미게 됩니다. 스트래지패턴에서 배웠던 상속보다는 구성이 여기서도 활용됩니다.
4. 행동데코레이션
커피주문앱에서 Whip
, Milk
등이 해당됩니다. 이 클래스들에겐 Beverage타입의 멤버변수가 있습니다.
public class Espresso extends Beverage{
public Espresso() {
description = "Espresso"; // description은 Beverage로 부터 상속받는
}
public double cost() { // 가격은 커스텀을 생각치 않고 에스프레소 가격만 입
return 1.99;
}
}
public class Milk extends BeverageDecorator{
Beverage beverage; // 데코레이션 할 음료를 저장하기 위한 iv
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescrption() + ", Milk";
}
public double cost() {
return .20 + beverage.cost(); // 음료 가격에 우유 요금 추
}
}
public class StartBucksCoffee {
public static void main(String[] args) { // 주문 시작
Beverage beverage = new Espresso(); // 아무것도 넣지 않은 에스프레스 주문
System.out.println(beverage.getDescrption()
+ " $" + beverage.cost());
Beverage beverage2 = new DarkRoast(); // 우유2, 휘핑 1 추가한 다크로스트
beverage2 = new Milk(beverage2);
beverage2 = new Milk(beverage2);
beverage2 = new Whip(beverage2);
System.out.println(beverage2.getDescrption()
+ "$" + beverage2.cost());
Beverage beverage3 = new HouseBlend(); // 두유1, 우유1, 휘핑1 추가한 하우스 블랜드
beverage3 = new Soy(beverage3);
beverage3 = new Milk(beverage3);
beverage3 = new Whip(beverage3);
System.out.println(beverage3.getDescription()
+ " $" + beverage3.cost());
}
}
}
특정 구성요소인지 확인한 다음 어떤 작업을 처리(ex. 하우스 블렌드 커피는 특별할인됨)하는 경우에 데코레이터 패턴을 사용하기 힘들수 있다.
클래스가 많이 추가되는 경우 남들이 봤을 때 이해하기 힘든 디자인이 만들어 질 수 있다.(자바 IO 클래스가 그렇다)
구성요소를 초기화하는 코드가 훨씬 복잡해진다.