装饰模式

意图

动态地给一个对象添加一些额外的职责

动机

首先理解一下什么是装饰,我有一杯珍珠奶茶,但是冬天比较冷,我将珍珠奶茶 加热 一下,变成一杯热的珍珠奶茶, 当然可能条件有很多种,夏天热,我需要冰的珍珠奶茶等等

怎么以装饰模式的思路设计呢, A(珍珠奶茶) 基础上进行自定义的装饰(加热), 得到一个新的对象 B(热的珍珠奶茶), 那么 B 就需要持有一个 A 的对象,因为我本质还是一杯珍珠奶茶 A,所以用户在喝热的珍珠奶茶时,实际上是在喝珍珠奶茶基础上添加了 B 的额外功能(加热)。
稍微复杂一点,那么此时我在 B 的基础上还可以在进一步装饰,还需要加一层奶盖,那么我在喝 加了奶盖的热的珍珠奶茶,代码层次的行为应该是,我喝加热的珍珠奶茶并且再此基础上添加额外的奶盖,喝热的珍珠奶茶也就是在喝珍珠奶茶的基础上额外的加热。

可能说的有点不清楚,装饰模式实际上给人一种一层一层包装的感觉,最核心是我们的珍珠奶茶,但是上面的装饰可以很灵活的自由添加,而且对用户而言,我对这些装饰后的对象和原对象奶茶都有一套相同的行为 ,用户是感觉不到装饰过的组件在行为上与未装饰组件间的差异

接下来先看一下 UML 图,结合 UML 可能有更深的理解

UML 图

uml

  • Component : 定义一个对象接口,可以给这些对象动态地添加职责
  • ConcreteComponent : 定义一个对象,可以给这个对象添加一些职责
  • Decorator : 维持一个指向 Component 对象的指针,并定义一个与 Component 接口一致的接口
  • ConcreteDecorator : 向组件添加指针

结合前面提到的例子,怎么理解呢?我们将对奶茶 的行为进行抽象,生成一个 Component 的基类, 珍珠奶茶在这里就是 ConcreteComponent, 对装饰这个行为进行抽象成 Decorator, 它需要保存一个珍珠奶茶的对象,也就是 ConcreteComponent(Component) 的对象, 而具体的装饰的部件例如 加冰 还是 加热,都是基于 Decorator 实现的具体装饰类

在客户端调用 的时候,就如 UML 图右边 note 提示伪代码那样,将请求转发给它的 Component 对象,并有可能在转发请求前后执行一些附加的动作

使用场景

  • 在不影响其它对象的情况下,以动态、透明的方式给单个对象添加职责
  • 处理那些可以撤销的职责
  • 当不能采用生成子类的方法进行扩充时
    • 为支持每一种组合将产生大量的子类,使得子类数目呈爆炸式增长
    • 因为类定义被隐藏,或类定义不能用于生成子类

来个例子

就用前面提到的奶茶的例子,首先先定义基类 奶茶

class Milktea
{
public:
    virtual ~Milktea(){}
    virtual void drink(){}
};

那么 珍珠奶茶 继承自 奶茶

#include <iostream>

class PearlMilktea: public Milktea
{
public:
    virtual void drink(){
        std::cout << "珍珠奶茶" << std::endl;
    }
};

接下来主要就是装饰类如何实现

#include "component.h"

class Decorator: public Milktea
{
public:
    Decorator(Milktea *tea) : m_milktea(tea) {}

    virtual void drink(){
        m_milktea->drink();
    }

private:
    Milktea *m_milktea;
};

具体的装饰类,比如加冰和加奶盖

class AddIce: public Decorator
{
public:
    AddIce(Milktea *tea) : Decorator(tea) {}

    virtual void drink(){
        addIce();
        Decorator::drink();
    }

private:
    void addIce() { std::cout << "加冰的";}
};

class AddMilkFoam: public Decorator
{
public:
    AddMilkFoam(Milktea *tea) : Decorator(tea) {}

    virtual void drink(){
        addMilkFoam();
        Decorator::drink();
    }

private:
    void addMilkFoam() { std::cout << "加奶盖的";}
};

那么就是剩下给珍珠奶茶动态的加料

#include "component.h"
#include "decorator.h"
#include <iostream>
using namespace std;

#define SAFE_DELETE(p) if(p){delete p; p=nullptr;}

int main()
{
    PearlMilktea *pPearlMilktea = new PearlMilktea();
    pPearlMilktea->drink();

    AddIce *pAddIce = new AddIce(pPearlMilktea);
    pAddIce->drink();

    AddMilkFoam *pAddMilkFoam = new AddMilkFoam(pAddIce);
    pAddMilkFoam->drink();

    SAFE_DELETE(pPearlMilktea);
    SAFE_DELETE(pAddIce);
    SAFE_DELETE(pAddMilkFoam);

    return 0;
}

执行结果

珍珠奶茶
加冰的珍珠奶茶
加奶盖的加冰的珍珠奶茶

析构

为什么单独将析构拎出来说一下,如果有仔细看例子中 main() 部分的代码,我们析构的时候分别对三个指针变量都进行了删除操作

但是一层一层装饰后,最理想的应该就是我将最外层 pAddMilkFoam 对象直接 delete 的就可以了,很类似于 组合模式 中,我删除一个枝节点,应该将它下面管理的所有叶节点也全部删除了

再看一下下面这个例子:

int main()
{
    AddIce *pAddIce = new AddIce(new PearlMilktea());
    pAddIce->drink();

    SAFE_DELETE(pAddIce);

    return 0;
}

我直接 new PearlMilktea() 传入了 AddIce() 的构造函数,想手动析构 PearlMilktea 对象都析构不了,所以我们需要管理传入的变量析构

最简单的解决方案,因为 Decorator 保存了一份传入的指针,交给他删除就可以了,所以定义一下析构函数,如下:

virtual ~Decorator(){
    if (m_milktea) delete m_milktea;
}

现在在多层的装饰情况下,删除最外层的对象时会一层一层向里删除,看上去挺美的,但是这样保险吗?

不保险,可以看一下下面的代码:

int main()
{
    PearlMilktea *pPearlMilktea = new PearlMilktea();
    pPearlMilktea->drink();

    AddIce *pAddIce = new AddIce(pPearlMilktea);
    pAddIce->drink();

    AddMilkFoam *pAddMilkFoam = new AddMilkFoam(pPearlMilktea);
    pAddMilkFoam->drink();

    SAFE_DELETE(pAddIce);
    SAFE_DELETE(pAddMilkFoam);

    return 0;
}

我一个 pPearlMilktea 对象分别被 pAddIcepAddMilkFoam 的构造使用,这样我删除的时候,按照上面的析构方式,必然会将 pPearlMilktea 删除2次,导致崩溃

但是看到一个指针被多处使用,但是不知道什么时候才能真正删除,想到了什么? 智能指针,带有引用计数的智能指针 share_ptr

具体实现可以参考一下这篇博客,我这里就不全部粘贴源码了
https://blog.csdn.net/walkerkalr/article/details/28633123

关于指针的管理,不仅仅只是析构的问题,再举个例子:

现在 Decorator 是通过构造的时候传入需要被装饰的对象直接生成了,并且没有提供类似于设置被装饰对象的接口 setComponent(Component *tmp)
假设我们提供了,那么对原有的指针如何管理,也需要根据实际情况设计,尽可能少的在使用过程中被误操作

总结

装饰模式在实现上很接近于代理模式,持有一个实际的对象,间接的去操作它,只是在装饰模式中代理的部分被抽象成装饰类的基类,将代理部分抽象到 Decorator 中,具体子类根据实际情况重载一部分自己感兴趣的函数。而这种实现使递归组合成为了装饰模式中一个必不可少的部分,当然,在实现过程中也需要关注一下析构的问题~