CS/디자인 패턴

Decorator Pattern

Ynghan 2023. 10. 24. 00:14

목차

  • 문제 상황에 대한 소개
  • 데코레이터 패턴의 발견
  • 데코레이터 패턴의 구조와 실행 동작원리
  • Java I/O에서의 데코레이터 적용

 

디자인 패턴

 

이 패턴은 다음과 같은 상황에서 사용됩니다:

1. 객체의 책임과 동작을 동적으로 변경해야 할 때.
2. 구체적인 구현을 책임과 동작에서 분리해야 할 때.
3. 서브클래싱(하위 클래스 생성)을 통해 변경을 달성하는 것이 비실용적이거나 불가능할 때.
4. 특정 기능이 객체 계층 구조의 상위에 존재해서는 안 될 때.
5. 구체적인 구현 주변에 많은 작은 객체가 존재하는 것이 허용될 때.
Decorator 패턴은 객체의 동작을 확장하거나 수정하기 위해 새로운 레이어(장식자)를 추가하는 방식으로 동작합니다. 이를 통해 객체의 책임과 동작을 동적으로 조정하고, 상속을 사용하지 않고도 객체의 기능을 확장할 수 있습니다. 이 패턴은 객체 지향 설계의 유연성을 향상시키고, 코드의 재사용성을 높이는 데 도움이 됩니다.

기존 설계

  • 음료 주문시스템에 사용되는 Beverage class : abstract class
  • 각각 음료를 구현하는 자식 Class : Beverage class에서 상속

모든 재료 구현 클래스는 Beverage 추상 클래스를 상속받는다.

이 때, Beverage 추상 클래스를 상속받는 조미료 구현 클래스가 늘어나게된다면?

음료 클래스와 조미료 클래스 모두 Beverage 클래스를 상속받는 상태이다.

또한, boolean 애트리뷰트와 getter/setter가 추가된다면?

Beverage 코드가 굉장히 복잡해지고, 코드 재사용성 역시 낮아진다.

 

cost() 메소드

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 += whipCost;
        return condimentCost;
    }
}

public class DarkRoast extends Beverage {
	public DarkRoast() {
    	description = "Excellent Dark Roast";
    }
    public float cost() {
    	return 1.99 + super.cost();
    }
}

 

설계에 영향을 미치는 예상되는 변화

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

OCP

• 클래스는 확장을 위해 열려 있어야 하지만 수정을 위해 닫혀 있어야 합니다
• 기존 코드를 수정하지 않고도 클래스를 쉽게 확장하여 새로운 동작을 통합할 수 있습니다
• 변화에 탄력적이고 변화하는 요구사항을 충족하기 위해 새로운 기능을 사용할 수 있을 정도로 유연합니다

주의: 모든 경우에 OCP(Open Closed Principle)를 적용하려고 하지 마십시오. 가능하면 간단한 디자인을 유지하세요!

데코레이터 패턴을 적용해보자.

우리는 '음료'로 시작해서 실행 시점에 '조미료'를 사용해 음료를 데코레이트 할 것입니다.
만약 손님이 Mocha와 Whip 있는 Dark Roast를 원한다면 다음을 따를 수 있습니다.

1. DarkRoast 개체 가져오기
2. 모카 개체로 데코레이트 하기
3. 휘핑 개체로 데코레이트 하기
4. cost( ) 메소드 호출하고 delegation에 따라 조미료 비용을 추가

데코레이터로 음료 주문 제작

1. DarkRoast 개체로 시작한다.

2. 고객이 Mocha를 원해서 Mocha 개체를 생성해서 DarkRoast에 래핑합니다.

3, 고객은 또한 Whip도 원해서 Whip 데코레이터를 생성해서 Mocha를 포장합니다.

4. 이제 고객의 비용을 계산해 보겠습니다. 가장 바깥쪽 데코레이터에 cost()를 호출하여 Whip을 실행하면 Whip은 자신이 장식하는 객체에 비용 계산을 위임(delegate)할 것입니다. 한 번 비용을 계산하면, Whip은 자신의 비용을 추가합니다.

 

데코레이터 아이디어 리뷰

  • 데코레이터는 자신의 행동을 추가합니다.
  • 하나 이상의 데코레이터를 사용하여 개체를 래핑할 수 있습니다.
  • 우리는 원래의(감싸져있지 않은) 객체 대신에 데코레이트된(장식된) 객체를 전달할 수 있습니다.
  • Decorators는 자신이 장식하는 객체와 동일한 슈퍼 타입(상위 형식 또는 인터페이스)을 가져야 합니다.
  • 우리는 원하는 개수의 데코레이터로 런타임에 사물을 동적으로 꾸밀 수 있습니다.

 

데코레이터 정의

Decorator 패턴은 동적으로 물체에 추가적인 책임을 부여합니다.
Decorator는 기능 확장을 위한 subclassing에 대한 유연한 해결책을 제공합니다.

 

 

 

 

 

 

 

 

 

예시 적용하기

 

Beverage class and Concrete Beverage class

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
    }
}

Condiments

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 Test Drive

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.getDesscription() + " $" + beverage3.cost());
    }
}
Beverage는 추상 클래스로, Decorator 패턴을 통해 객체의 동작을 확장할 수 있습니다. 코드에서 beverage2는 DarkRoast 객체로 시작하고, 그런 다음 이를 Mocha 데코레이터로 래핑하고 또 다른 Mocha 데코레이터로 래핑하며, 마지막으로 Whip 데코레이터로 래핑합니다. 이것은 beverage2 객체에 다양한 데코레이터를 연속적으로 적용하여 음료의 특성을 변경하고 확장하는 것을 나타냅니다.

내 생각

Strategy 패턴과 Decorator 패턴이 헷갈렸는데,

  • Strategy 패턴은 객체의 행위를 정의하고 여러 구현을 교체할 수 있도록 설계하며, 객체의 행위를 변경하는 데 중점을 둠
  • Decorator 패턴은 객체의 동작을 확장하거나 수정하는 데 중점을 둠
  • Strategy 패턴은 인터페이스와 구현 클래스로 이루어지고, 상속을 사용하지 않음
  • Decorator 패턴은 컴포넌트와 데코레이터 클래스로 이루어지며, 클래스 계층 구조를 이용해 동작을 확장함
    • Component (Interface) : 원본 객체와 장식된 객체 모두를 묶는 역할
    • ConcreteComponent : 원본 객체 (데코레이팅 할 객체)
    • Decorator : 추상화된 장식자 클래스
      • 원본 객체를 가리키는 필드 와 인터페이스의 구현 메소드를 가지고 있다.
    • ConcreteDecorator : 구체적인 장식자 클래스
      • 부모 클래스가 감싸고 있는 하나의 Component를 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.

 

적용하는 방식

Decorator 패턴을 적용하는 방법은 간단히 추상 클래스를 상속받는 추상 클래스를 만들고 상위 추상 클래스를 상속받는 구현체 클래스에 하위 추상 클래스를 상속받는 구현체 클래스를 적용하면 될 것 같다.

 


[참조] https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0Decorator-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90#decorator_pattern