Learn/Architecture

[Design Pattern] Decorator Pattern

push and sleep 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할 수 있다. 
  • 자잘한 클래스를 여러 개 만들고 해당 패턴을 모르는 사람은 이해하기 어려운 단점이 있다.