reference: "모던 C++ 디자인패턴" / 드미트리 네스터룩
커레이터 패턴에서 객체들에 새로운 기능을 확장할 때 활용되는 매커니즘이 바로 컴포지션(composition)이다. 이 컴포지션은 두 가지 방식으로 나누어진다.
1. 동적 컴포지션
참조를 주고받으며 런타임 과정에서 동적으로 무언가(기능)를 합성하게 한다. 이 방식은 유연성을 제공하는데, 예를 들어 사용자 입력에 따라 컴포지션에 만들 수 있다.
2. 정적 콤포지션
템플릿을 이용하여 컴파일 시점에 추가기능이 합성되게 한다. 이는 코드 작성 시점에 객체에 대한 정확한 추가 기능 조합을 요구하게 된다.
새로운 기능(도형에 색상을 입힌다거나 투명도를 입히는 것)을 상속 대신 컴포지션으로 만들 수 있다.
빨간색 원 객체는 다음 코드와 같이 만들어질 수 있다.
Circle circle{0.5f};
ColoredShape redCircle{circle, "red"};
cout << redCircle.str();
// => "A circle of radius 0.5 has the color red"
그리고 이 빨간색 원에 투명도를 추가하고 싶다면 다음 코드와 같이 작성할 수 있다.
..
TransparentShape redNtransCircle{redCircle, 85};
cout << redNtransCircle.str();
// => "A circle or radius 0.5 has the color red has xx% transparency"
이처럼 동적 데커레이터를 통해 기능을 동적으로 생성할 수 있다.
클라이언트는 데커레이터를 통해서 resize() 인터페이스를 사용할 수 없다.
Circle circle{3};
ColoredShape redCircle(circle, "red"};
redCircle.resize(2); // 컴파일 에러
데커레이션된 객체의 멤버 함수와 필드에 모두 접근할 수 있어야 한다면 어떻게 해야할까? 바로 믹스인 상속 방식을 활용하면 된다.
믹스인(MixIn) 상속: 템플릿 인자로 받은 클래스를 부모 클래스로 지정하는 방식
template <typename T>
class ColoredShape : T {
public:
// 템플릿 파라미터를 제약할 방법은 없다.
// static_assert를 이용하여 Shape 이외의 타입이 지정되는 것을 막는다.
static_assert(is_base_of<Shape, T>::value,
"Template argument must be a Shape");
string color;
string str() const override {
ostring oss;
oss << T::str() << " has the color " << color;
return oss.str();
}
};
Circle과 같은 구체 클래스를 상속받을 수 있다.
그렇다면 resize() 같은 메서드를 호출할 수 있다는 뜻이다.
// 클라이언트 코드
ColoredShape<TransparentShape<Circle>> circle{"blue"};
circle.size = 2;
circle.transparency = 0.5;
cout << circle.str();
// 믹스인 상속 후 어떤 멤버든 접근 가능
circle.resize(3);
믹스인 상속 후 어떤 멤버로든 접근이 가능하나, 모든 생성자를 한번에 호출하던 부분을 잃게 되었다. 이를 '제네릭 파라미터 팩'을 활용하여 해결할 수 있다.
// TransparentShape
template <typename T>
class TransparentShape : T {
public:
static_assert(is_base_of<Shape, T>::value,
"Template argument must be a Shape");
uint8_t transparency;
string str() const override {
// ...
}
// 제네릭 파라미터 팩을 받는 생성자 추가
// 생성자의 첫번째 파라미터는 현재의 템플릿 클래스에 적용됨.
// 두번째 인자들은 부모 클래스에 전달될 제네릭 파라미터 팩.
TransparentShape(const uint8_t transparency, Args ...args)
: T(std::forward<Args>(args)...), transparency{ transparency }{}
};
// ColoredShape 클래스도 마찬가지로 제네릭 파라미터 팩을 받는 생성자 추가
// ..
// 클라이언트 코드
ColoredShape<TransparentShape<Circle>> circle{ "red", 51, 5 };
cout << circle.str() << endl;
Decorator pattern은 OCP를 준수하면서도 클래스에 새로운 기능을 추가할 수 있게 해준다.
데커레이터의 핵심은 데커레이터들을 합성할 수 있다는 것이다. 하나의 객체는 복수의 데커레이터들을 순서와 무관하게 적용할 수 있다.
동적 데커레이터
: 데커레이션할 객체(기능)의 참조를 저장하고 런타임에 동적으로 합성할 수 있다. 대신 원본 객체(ex, Circle)가 가진 멤버들(ex, resize())에 접근할 수 없다.
정적 데커레이터
: 믹스인 상속(템플릿 파라미터를 통한 상속)을 이용해 컴파일 시점에 데커레이터를 합성한다. 런타임 융통성을 가질 수 없지만, 원본 객체 멤버들에 접근할 수 있는 장점이 있다. 그리고 생성자 포워딩을 통해 객체를 완전하게 초기화할 수 있다.