디자인 패턴/GoF

[디자인 패턴] 구조 패턴 (4) 장식자 Decorator

로파이 2021. 1. 28. 15:31

장식자 (Decorator)

 

1. 의도

  • 객체에 새로운 기능을 동적으로 추가하는 방법

2. 활용

  • 기존에 있던 인터페이스에 기능을 추가하고 싶을 때 사용한다.
  • 특정 클래스에 추가적인 기능을 포함하고 있는 서브 클래스를 만드는 것을 피한다.

장식자 패턴 예시

  • 예시로 GUI에 표시하는 시각적 객체들을 위한 VisualComponent 클래스가 있다하자.
  • 그렇다면 VisualComponent 내부의 클래스 계통에 있는 모두에게 기능을 추가하는 것이 아니라 특정 서브 클래스에만 기능을 추가하는 것이다.
  • VisualComponent 클래스를 상속하는 특정 서브 클래스TextView라고 하자.

TextView 인터페이스에 스크롤바와 테두리 기능을 추가하고 싶을 때, TextView를 상속하는 새로운 서브 클래스를 만들어야할까?

  • 당연히 그렇게 만든다면 프레임 워크가 복잡해지기 마련이다. 또한 TextView 말고도 다른 서브 클래스가 두 기능을 이용하고 싶을지 모른다.
  • 우리는  스크롤바와 테두리 기능을 포괄하는 새로운 추상 클래스 Decorator를 만들고 이를 VisualComponent에 상속시킨다.
  • VisualComponent에 상속하는 이유는 사용자 인터페이스에 일치시키 위함이다.
  • 이는 사용자가 VisualComponent가 실제로 TextView 클래스인지, 기능을 추가한 Decorator 클래스인지 모르게 하고 사용자 인터페이스 VisualComponent::Draw() 라는 연산을 통해 GUI에 해당 시각 객체를 그릴 수 있을 것이다.

 

3. 참여자

 

  • Component: 기존 사용자 인터페이스를 제공하는 추상 클래스
  • ConcreteComponent: 사용자 인터페이스 종류 중 하나
  • Decorator: 기능에 관련된 클래스 계통을 만들고 Component를 상속한다. 가적으로 Component 인스턴스를 참조하는 참조자를 가지고 있다.
  • ConcreteDecorator: Component에 새롭게 추가할 기능을 실제로 구현하는 클래스

Decorator에서 Component 인스턴스를 가지고 있는 (객체 합성) 이유는 무엇 인가

  • 구조 패턴에서 자주 등장하는 객체 합성은 자신이 또 다른 클래스 인스턴스를 포인터나 리스트로 가지고 있었다.
  • 가교 패턴에서는 추상 클래스가 자신의 구현 연산을 담당하는 구현부 클래스를 소지하고 있음으로 구현 기능을 사용했고 복합체 패턴에서는 한 객체가 같은 클래스의 객체들을 컨테이너로서 포함하고 있을 때 리스트형태로 합성하였다.

장식자 패턴에서는 가교 패턴과 비슷하게 합성하고 있는 객체의 구현을 그대로 사용하기 위해 해당 객체의 포인터를 관리한다.

 

예시로

// Decorator의 Operation 연산 (Component과 일치하는 인터페이스)

virtual void Operation();

void Decorator::Operation()

{

       // Decorator가 합성하고 있는 Component 객체의 인터페이스를 호출

       _component->Operation();

      // 자신의 기능을 실행

       AddedBehavior();

}

 

그렇다면 어떻게 스크롤바와 테두리 기능을 TextView 클래스에 추가할 수 있을까?

이해를 돕기 위해 이번 장식자 패턴에서는 코드 예시를 먼저 본다.

 

VisualComponent.h

class VisualComponent
{
public:
	VisualComponent();

	virtual void Draw();
	virtual void Resize();
	// ...
};

 

만약 TextView 클래스를 구현한다면, TextView 클래스는 VisualComponent를 상속받아 Text를 띄우는 Draw() 연산을 구현한다. (Resize()도 마찬가지)

 

Decorator.h

#include "VisualComponent.h"
class Decorator : public VisualComponent
{
public:
	Decorator(VisualComponent* component) : _component(component) {}
	virtual void Draw() { _component->Draw(); }
	virtual void Resize() { _component->Resize();}
	// ...
private:
	VisualComponent* _component;
};

여기서 중요한 것은 Decorator가 VisualComponent 인스턴스를 포인터로 소유하는 것이다.

사용자가 VisualComponent 인스턴스의 일반적인 Draw() 연산 호출 시

인스턴스의 실제 클래스가 Decorator 클래스라면 자신이 소유하고 있는 _component->Draw() 연산을 먼저 호출하는 의미이다.

 

그 _component 인스턴스는 실제 TextViw 클래스 일 수 도 있고 또 다른 Decorator 클래스 일 수 도 있다.

그 의미를 알아보기전에 Decorator 클래스를 상속하는 파생 클래스 ScrollDecorator와 BorderDecorator를 정의해보면 다음과 같다.

class BorderDecorator : public Decorator
{
public:
	BorderDecorator(VisualComponent* component, int borderWidth) : Decorator(component), _width(borderWidth)
	{}
	virtual void Draw()
	{
		Decorator::Draw();
		DrawBorder(_width);
	}
private:
	void DrawBorder(int);
	int _width;
};
#include "Decorator.h"
class ScrollDecorator : public Decorator
{
public:
	ScrollDecorator(VisualComponent * component, int scrollPos) : Decorator(component), _scrollPos(scrollPos){}

	virtual void Draw()
	{
		Decorator::Draw();
		ScrollTo(_scrollPos);
	}
private:
	void ScrollTo(int);
	int _scrollPos;
};

ScrollDecorator와 BorderDecorator의 클래스 정의에서 VisualComponent* 타입의 인스턴스를 받아 Decorator의 매개변수 생성자를 통해 부모 클래스를 초기화 한다.

쉽게 트리 구조로 비교하자면 즉 component 인스턴스는 현재 인스턴스의 부모를 참조하는 포인터이고 Draw() 호출시

자신의 부모 Decoraotr::Draw()를 먼저 호출하고 자신의 기능에 관련된 DrawBorder(int), ScrollTo(int)를 실행한다.

 

예시를 들어보면, 사용자 측에서 최종 두 기능이 추가된 TextView를 사용하고자 하면 다음과 같이 나타내진다.

	TextView* textView = new TextView;
	VisualComponent* adTextView = new BorderDecorator(new ScrollDecorator(textView, 1), 5);
	
	// draw textView with Bordered, scrollbar
	adTextView->Draw();

adTextView는 BorderDecorator 인스턴스로 다음과 같은 구조를 가진다.

  • BoderDecorator는 ScrollDecorator 인스턴스를 소유하고 있고 ScrollDecorator는 TextView를 인스턴스를 가지고 있다.
  • 만약 사용자 측에서 adTextView->Draw()와 같이 인터페이스를 호출했다면 다음과 같은 과정이 일어난다.
  • Decorator::Draw()는 _component->Draw()로 치환 가능하다.

Draw() 호출 과정

  • 즉 장식자 기능은 원본 기능이 먼저 실현된 후 차근차근 실현된다.

4. 구현

장식자 패턴을 사용할 때 고려할 사항

 

1) 인터페이스를 일치시킨다.

  Decorator도 하나의 Component로 보고 인터페이스의 구현은 소유한 Component 인스턴스의 연산을 이용한다.

 

2) 추상 클래스로 정의되는 Decorator 클래스 생략하기

  Decorator는 추상 클래스로 구현이 없어도 되고 Decorator 파생 클래스와 합쳐 사용한다.

 

3) Component 클래스는 가벼운 무게를 유지

  최소한의 인터페이스를 유지하고 내부 멤버 변수를 두지 않는다. 서브 클래스에서 확장성을 높이기 위해

 

4) 전략 패턴과 비교

  장식자 패턴은 겉에 기능을 씌워 맨 나중에 기능을 실현하지만 전략 패턴은 가장 먼저 실행되고 Component 내부 값이 변경될 수 있다.