프로그래밍 일반/디자인 패턴

데커레이터 패턴(Decorator Pattern)

지노윈 2020. 1. 28. 22:42
반응형

동료가 작성한 코드의 원본을 수정하지 않고 기능을 확장하는 방법은?

가장 쉽게 생각나는 것이 "상속"을 이용하는 것입니다.

데커레이터 패턴은 이미 존재하는 타입에 새로운 기능을 추가하면서도 원래 타입의 코드에 수정을 피할 수 있게 해줍니다. (OCP 준수)

 

Shape을 상속 받아 ColoredShape과 TransparentShape을 추가하고 두 기능이 모두 필요하여 ColoredTransparentShape을 만들었습니다.

이렇게 상속의 방법으로 기능을 추가 한다면 Shape의 기능이 들어 나거나 도형이 추가된다면 클래스가 점점 수도 없이 늘어나야만 합니다.

Square, Circle 등과 같은 도형추가 되거나 크기 조절 가능 기능을 추가되는 것을 상상해 봅시다.

평범한 상속으로는 효율적으로 원본을 수정하지 않고 기능을 확장하기 힘들다는 것을 알 수 있습니다.

 

  • 동적 구성(Composition)
    참조를 주고 받으며 런타임에 동적으로 추가 기능을  구성합니다.  사용자에 의해 추가 기능이 결정되므로 최대한의 유연한 방법입니다.
  • 정적 구성(Composition)
    템플릿을 이용하여 컴파일 시점에 추가 기능을 구성합니다. 코드 작성 시점에 정확한 추가 기능 조합이 결정됩니다.

동적 데커레이터

상속 대신 구성으로 ColoredShape을 만듭니다. 이미 생성된 Shape 객체의 참조로 새로운 기능을 추가합니다.

struct ColoredShape : Shape
{
  Shape& shape;     // 구성, 참조로 새로운 기능 추가
  string color;

  ColoredShape(Shape& shape, const string& color)
    : shape{shape}, color{color}
  {
  }

  string str() const override
  {
    ostringstream oss;
    oss << shape.str() << " has the color " << color;
    return oss.str();
  }
};
struct Shape
{
  virtual string str() const = 0;
};

struct Circle : Shape
{
  float radius;

  explicit Circle(const float radius)
    : radius{radius}
  {
  }

  void resize(float factor)
  {
    radius *= factor;
  }

  string str() const override
  {
    ostringstream oss;
    oss << "A circle of radius " << radius;
    return oss.str();
  }
};

 

다음은 동적으로 Circle이 ColoredShape로 구성되는 예입니다. 즉, 빨간 도형에 원이 구성되어 빨간 원이 되었습니다.

Circle circle {0.5};
ColoredShape redCircle {circle, "red"};
cout << redCircle.str();

// 출력 결과: A cirlce of radius 0.5 has the color red

 

마찬가지 방법으로 다음과 같이 쉽게 투명도를 가지게 할 수 있습니다.

struct TransparentShape : Shape
{
  Shape& shape;
  uint8_t transparency;

  TransparentShape(Shape& shape, const uint8_t transparency)
    : shape{shape}, transparency{transparency}
  {
  }

  string str() const override
  {
    ostringstream oss;
    oss << shape.str() << " has "
      << static_cast<float>(transparency) / 255.f*100.f
      << "% transparency";
    return oss.str();
  }
};

다음은  동적으로  Square가  TransparentShape로 구성되는 예입니다. 즉, 투명도형에 정사각형이 구성되어 투명한 정사각형이 되었습니다.

Square square{3};
TransparentShape demiSquare{square, 85}
cout << demiSquare.str();

// 출력 결과 : A square with side 3 has 33.333% transparency

같은 원리로 Circle → ColoredShape → TransparentShape으로 구성하여 동적으로 편리하게 새로운 도형을 만들 수 있습니다.

TransParentShape myCircle {
  ColoredShape {
    Circle{23}, "greenn"
  }, 64
};
count << myCircle.str();

// 출력 결과 : A circle of radius 23 has the color green has 25.098% transparency

정적 데커레이터

Circle에는 resize 메서드가 있으며 아래 코드는 컴파일 오류가 납니다. 

Circle circle{3};
ColoredShape redCircle {circle, "red"};
redCircle.resize(2); // 컴피일 에러

데커레이션된 객체의 멤버 함수와 필드에 모두 접근 할 수 있어야 한다면 어떻게 해야 할까요?

MixIn 상속을 사용하여 구현 할 수 있습니다. MixIn은 이미 MCDP 1회차에서 소개하였습니다.

 

Mixin 상속은 템플릿 인자로 받은 클래스를 부모 클래스로 지정하는 방식입니다.

Mixin 상속을 이용한 ColoredShape 구현 입니다.

template <typename T> struct ColoredShape : T         // MixIn, 템플릿 인자 T가 부모 클래스 T로 지정 되었습니다.
{
  static_assert(is_base_of<Shape, T>::value,
    "Template argument must be a Shape");

  template <typename...Args>
  ColoredShape(const string& color, Args ...args)
    : T(std::forward<Args>(args)...), color{color}  
  {
  }

  string color;

  string str() const override
  {
    ostringstream oss;
    oss << T::str() << " has the color " << color;
    return oss.str();
  }
};

[객체 지향 프로그래밍] - Mixin 이란?

 

Mixin 이란?

자신을 템플릿 인자로 하여 원하는 클래스들을 재조립하여 새로운 클래스 타입을 만들어요. 즉, 각각의 기능을 컴포넌트 처럼 구현하고 원하는 기능들을 조합하여 새로운 타입을 만들 수 있어요. 상속을 이용하면..

devjino.tistory.com

Shape 파생 클래스만 템플릿 인자로 올 수 있도록 is_base_of로 체크하였습니다.

enable_if_t를 사용하여 다음과 같이 제한 할 수도 있습니다.

template <typename T, typename = enable_if_t<is_base_of_v<Shape, T>>>

동일한 방식으로 TransparentShape 구현하였습니다.

template <typename T> struct TransparentShape : T
{
  uint8_t transparency;
  template<typename...Args>
  TransparentShape(const uint8_t transparency, Args ...args)
    : T(std::forward<Args>(args)...), transparency{ transparency }
  {
  }

  string str() const override
  {
    ostringstream oss;
    oss << T::str() << " has "
      << static_cast<float>(transparency) / 255.f * 100.f
      << "% transparency";
    return oss.str();
  }
};

ColoredShape<T>와 TransparentShape<T>의 구현을 기반으로 하여 색상이 있는 투명한 도형을 아래와 같이 함성 할 수 있습니다.

TransparentShape<ColoredShape<Square>> square{ 85, "blue", 10 };
square.color = "blue";
square.resize(20);

너무나 아름답습니다.  이것이 템플릿을 이용한 코드 구성으로 구현한 정적 데코레이터입니다.

함수형 데커레이터

데커레이터 패턴은 클래스를 적용 대상으로 하는 것이 보통이지만 함수에도 동등하게 적용 할 수 있습니다.

함수 실행 앞/뒤에 로그를 남기는 기능을 구현할 경우 데커레이터 적용을 살표봅니다.

함수의 인자와 리턴값이 모두 있는 함수에 대해 로그 구현은 다음과 같습니다.

template <typename R, typename... Args>
struct Logger<R(Args...)>
{
  Logger(function<R(Args...)> func, const string& name)
    : func{func},
      name{name}
  {
  }

  R operator() (Args ...args)
  {
    cout << "Entering " << name << endl;
    R result = func(args...);
    cout << "Exiting " << name << endl;
    return result;
  }

  function<R(Args ...)> func;
  string name;
};

template <typename R, typename... Args>
auto make_logger(R (*func)(Args...), const string& name)    // 함수 데커레이션
{
  return Logger<R(Args...)>(
    std::function<R(Args...)>(func),
    name);
}

double add(double a, double b)
{
  cout << a << "+" << b << "=" << (a + b) << endl;
  return a + b;
}

add 함수를 데커레이션 하여 호출하는 코드는 다음과 같습니다.

auto logged_add = make_logger(add, "Add");
auto result = logged_add(2, 3);

요약

데커레이터 패턴은 OCP를 준수하면서도 클래스에 새로운 기능을 추가할 수 있게 해줍니다.

데커레이터의 핵심적인 특지은 데커레이터들을 합성할 수 있다는 것입니다.

 

 

[독서 리뷰] - 모던 C++ 디자인 패턴

 

모던 C++ 디자인 패턴

[객체 지향 프로그래밍/디자인 패턴] - 빌더 패턴(Builder Pattern) [분류 전체보기] - 디자인 패턴 [객체 지향 프로그래밍/디자인 패턴] - 팩토리 패턴(Factory pattern) [객체 지향 프로그래밍/디자인 패턴] -..

devjino.tistory.com

코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
 

Welcome to C++ Design Patterns’s documentation! — C++ Design Patterns 0.0.1 documentation

© Copyright 2018, Hans-J. Schmid Revision 786c83f8.

cpp-design-patterns.readthedocs.io