装饰器模式

装饰器模式(Decorator)

通常情况下需要扩展一个类的时候我们会想到继承:设计一个派生类添加想要的功能,甚至覆写父类的虚函数。

但这并不总是最有效的方法,例如我们很少去继承 std::vector ,因为其缺少虚析构函数。继承不起作用的关键原因是我们需要实现多个强化的功能,并且由于单一职责原则,我们希望将这些功能分开。

此时装饰器(Decorator)模式允许我们在既不修改原始类型(违背开闭原则)也不会产生大量派生类的情况下强化既有类型的职责和功能。

装饰器模式的典型结构

┌─────────────────┐
│   Component     │ (抽象组件接口)
│  <<interface>>  │
├─────────────────┤
│ + operation()   │
└────────▲────────┘
         │
         │ 实现
    ┌────┴────────────────────┐
    │                         │
┌───┴──────────────┐  ┌───────┴────────┐
│ConcreteComponent │  │   Decorator    │ (装饰器基类)
├──────────────────┤  ├────────────────┤
│+ operation()     │  │- component     │ (持有Component引用)
└──────────────────┘  │+ operation()   │
                      └───────▲────────┘
                              │
                              │ 继承
                    ┌─────────┴──────────┐
                    │                    │
            ┌───────┴─────────┐  ┌───────┴─────────┐
            │DecoratorA       │  │DecoratorB       │
            ├─────────────────┤  ├─────────────────┤
            │+ operation()    │  │+ operation()    │
            │+ addedBehavior()│  │+ addedBehavior()│
            └─────────────────┘  └─────────────────┘
角色 职责 说明
Component 抽象组件 定义对象接口,可以给这些对象动态添加职责
ConcreteComponent 具体组件 定义具体对象,装饰器可以给它添加额外职责
Decorator 抽象装饰器 持有一个Component对象,并实现Component接口
ConcreteDecorator 具体装饰器 向组件添加具体职责

其工作流程大致如下:

客户端请求 → ConcreteDecoratorB → ConcreteDecoratorA → ConcreteComponent
                    ↓                      ↓                    ↓
                添加功能B              添加功能A            核心功能
                    ↓                      ↓                    ↓
                    ←──────────────────────←────────────────────
                              返回结果(层层包装)

装饰器模式一般有两种实现模式:

  • 动态组合 允许在运行时组合某些东西,通常是通过按引用传递实现的。它的灵活性强,因为组合可以在运行时相应用户的输入。
  • 静态组合 意味着对象及其强化功能是在编译期时使用模板组合而成的。这意味着在编译时需要知道对象确切的强化功能,因为之后无法对其进行修改。

动态装饰器

考虑为一个 Shape 类扩展关于颜色的功能。使用组合而不是继承实现 Colored-Shape,传入一个 Shape 的引用,这时候 ColoredShape 就可以在已构造好的 Shape 上强化其功能:

struct Shape
{
    virtual std::string str() const = 0;
};

struct Circle : Shape
{
    float radius;
    explicit Circle(const float radius)
	    :radius(radius)
    {}

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

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

struct ColoredShape : Shape
{
    Shape& shape;
    std::string color;

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

    std::string str() const override
    {
        std::ostringstream oss;
        oss << shape.str() << " has the color " << this->color;
        return oss.str();
    }

    void make_dark()
    {
        if (constexpr auto dark = "dark "; !color.starts_with(dark))
            color.insert(0, dark);
    }
};

可以发现,ColoredShape 本身也是一个 Shape,同时组合了一个它装饰的 Shape 的引用。其中 constexpr、if 初始化和 运用C++20的 starts_with() 去创建了一个成员函数 make_dark()。

此时可以如下使用这个装饰器:

    Circle circle{ 0.5f };
    ColoredShape redCircle{ circle,"red" };
    std::cout << redCircle.str()<<std::endl;
    // A circle of radius 0.5 has the color red
    redCircle.make_dark();
    std::cout << redCircle.str() << std::endl;
    // A circle of radius 0.5 has the color dark red

此时我们想要给 Shape 增加一个透明度的强化功能,那么如下:

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

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

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

此时根据我们组合模式的原理,我们可以设计如下同时具有透明度和颜色属性的圆形:

 Circle c{ 5 };
 ColoredShape cs{ c,"green" };
 TransparentShape myCircle{ cs,64 };
 std::cout << myCircle.str();
 // A circle of radius 5 has the color green has 25.098% transparency

但是如此写是需要修改代码的:

TransparentShape t{ColoredShape{Circle{23},"green"},64};

这里是编译器是会报错的(当然这取决于编译器),因为我们这里并不能将临时对象传给一个引用,这会造成悬空。

静态装饰器

在上面创建的示例中有一个很有趣的地方,可以试试去调用一下 Circle 中的 resize()。

实际上 resize() 并不是 Shape 接口的一部分,我们并不能在装饰器中调用它。但是如果我们想要访问被装饰对象的属性成员和成员函数,我们可以通过模板,使用 mixin 继承的方式也就是类继承自它的模板参数来实现:

/* 
template<typename T>
concept ShapeConcept = requires(const T t) {
    { t.str() } -> std::convertible_to<std::string>;
} && std::is_base_of_v<Shape, T>;
*/

template<typename T> 
struct ColoredShape : T
{
    static_assert(std::is_base_of_v<Shape, T>, "Template argument must be a Shape");

	std::string color;

    ColoredShape(const std::string& color)
	    :color(color)
	{}

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

    void make_dark()
    {
        if (constexpr auto dark = "dark"; !color.starts_with(dark))
            color.insert(0, dark);
    }
};

template<typename T>
struct TransparentShape : T
{
    static_assert(std::is_base_of_v<Shape, T>, "Template argument must be a Shape");
    uint8_t transparency;
    TransparentShape(){};
    TransparentShape(const uint8_t transparency)
	    :transparency(transparency)
	{}

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

我们通过让类继承其模板参数,使其一一具备了我们需要的模板的成员函数和成员变量,同时可以通过 static_assert 或 concept-require 语句去约束模板参数,来保证 T 继承自 Shape

    ColoredShape<TransparentShape<Circle>> circle{ "green" };
    circle.radius = 2;
    circle.transparency = 0.5;
    std::cout << circle.str()<<std::endl;
    // A circle of radius 2 has 0% transparency has the color green
    circle.resize(2);
    std::cout << circle.str();
    // A circle of radius 4 has 0% transparency has the color green

但是这样的方法也有缺点,我们并没有充分利用构造函数,使得我们只能初始化最外层的类,不能够一行代码就完成这个类的实现。

为了实现这个功能,我们需要构建两者的转发构造函数。这些构造函数将接受两个参数:第一个是特定于当前模板类的参数,第二个是将转发给基类的通用参数包:

template<typename ...Args>
TransparentShape(const uint8_t transparency,Args... args)
    :T(std::forward<Args>(args)...),transparency(transparency)
{}
// same for ColoredShape

需要注意的是初始化的顺序,我们需要先为 T 的构造函数转发参数包,并且需要注意构造函数参数的数量必须准确,否则会因为无法匹配而编译失败。

还有一个问题是不能将这些构造函数声明为显式的(explicite),否则当多个装饰器组合起来后将会被C++的复制列表初始化规则困扰。