意图
动态地给一个对象添加一些额外的职责
动机
首先理解一下什么是装饰,我有一杯珍珠奶茶,但是冬天比较冷,我将珍珠奶茶 加热 一下,变成一杯热的珍珠奶茶, 当然可能条件有很多种,夏天热,我需要冰的珍珠奶茶等等
怎么以装饰模式的思路设计呢, A
(珍珠奶茶) 基础上进行自定义的装饰(加热), 得到一个新的对象 B
(热的珍珠奶茶), 那么 B
就需要持有一个 A
的对象,因为我本质还是一杯珍珠奶茶 A
,所以用户在喝热的珍珠奶茶时,实际上是在喝珍珠奶茶基础上添加了 B
的额外功能(加热)。
稍微复杂一点,那么此时我在 B
的基础上还可以在进一步装饰,还需要加一层奶盖,那么我在喝 加了奶盖的热的珍珠奶茶,代码层次的行为应该是,我喝加热的珍珠奶茶并且再此基础上添加额外的奶盖,喝热的珍珠奶茶也就是在喝珍珠奶茶的基础上额外的加热。
可能说的有点不清楚,装饰模式实际上给人一种一层一层包装的感觉,最核心是我们的珍珠奶茶,但是上面的装饰可以很灵活的自由添加,而且对用户而言,我对这些装饰后的对象和原对象奶茶都有一套相同的行为 喝
,用户是感觉不到装饰过的组件在行为上与未装饰组件间的差异
接下来先看一下 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
对象分别被 pAddIce
和 pAddMilkFoam
的构造使用,这样我删除的时候,按照上面的析构方式,必然会将 pPearlMilktea
删除2次,导致崩溃
但是看到一个指针被多处使用,但是不知道什么时候才能真正删除,想到了什么? 智能指针,带有引用计数的智能指针 share_ptr
具体实现可以参考一下这篇博客,我这里就不全部粘贴源码了
https://blog.csdn.net/walkerkalr/article/details/28633123
关于指针的管理,不仅仅只是析构的问题,再举个例子:
现在 Decorator
是通过构造的时候传入需要被装饰的对象直接生成了,并且没有提供类似于设置被装饰对象的接口 setComponent(Component *tmp)
假设我们提供了,那么对原有的指针如何管理,也需要根据实际情况设计,尽可能少的在使用过程中被误操作
总结
装饰模式在实现上很接近于代理模式,持有一个实际的对象,间接的去操作它,只是在装饰模式中代理的部分被抽象成装饰类的基类,将代理部分抽象到 Decorator
中,具体子类根据实际情况重载一部分自己感兴趣的函数。而这种实现使递归组合成为了装饰模式中一个必不可少的部分,当然,在实现过程中也需要关注一下析构的问题~