기타/디자인패턴

[디자인패턴] Decorator Pattern

hh_lin 2019. 10. 15. 00:31

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을 바탕으로 만들어졌다.

 

 

...