Decorator 装饰模式
意图
动态地给一个对象添加一些额外的职责,就增加功能来说,Decorator模式相比生成子类更为灵活。
Decorator(装饰模式)也被称作Wrapper(包装器)。
Decorator模式是对组件间协作的一种规划,它旨在解决为某个对象添加附加功能时导致的不够灵活、以及子类爆炸的问题。通过采用组合而非继承的手法,Decorator模式实现了在运行时动态扩展功能的能力,而且可以根据需要扩展多个功能。避免使用继承带来的**“灵活性差”和“多子类衍生问题”**。
从稳定-变化的角度来讲,在对象与其功能工作关系稳定的情况下,Decorator模式优化的是出于不断变化的“子功能”模块。
听起来很费解?我们从实际代码再来理解这个问题。
代码案例
现在,我们想要让一个文本编辑器(比如Markdown编辑器)拥有更加多变的文字显示方式,比如斜体、黑体、高亮、上标…等等。更重要的是,我们希望这些功能能够公用,比如***斜体加黑体***、==上标高亮==等组合功能。我们该如何实现呢?、
惯例,我们先看错误示范:
class Text
{
}
class Font : public
{
virtual void operate() = 0;
}
class BlackFont: public Font{ void operate(){...} }//黑体
class ItalianFont: public Font{ void operate(){...} }//斜体
class HighlightFont: public Font{ void operate(){...} }//高亮
class UpperFont: public Font{ void operate(){...} }//上标
class BlackItalianFont: public Font{ void operate(){...} }//黑体+斜体
class UpperItalianFont: public Font{ void operate(){...} }//上标+斜体
class BlackItalianUpperFont: public Font{ void operate(){...} }//黑体+斜体+上标
...
可以看到,在上面这种最基础的子类扩展写法中,我们需要为每一种功能组合都编写一个子类。
在添加新的功能时,我们只需要增加类而不需要对现有子类进行修改,这一点满足了扩展代替更改的原则以及面向接口编程的原则。但是,对应而来的问题是,当对象被赋予的功能过多时,子类的数量将会达到空前的规模,事实上,对于一个用于n种子功能的对象,我们要编写2n-1个子类去描述它的所有功能组合。
这对于维护人员来讲,这一无比庞大的维护工作是无法忍受的(想象由你来维护一个拥有10个功能的对象,你就有210-1 = 1023个子类需要照看…这还不赶紧跳槽?)。
此外,如此庞大的子类树也反应了代码可重用性的低下。可以想见,BlackItalianFont 类无非是 BlackFont 和 ItalianFont两个类的代码组合而已,实现起来必然也只是复制粘贴。
最后,我们来看一下上面这种写法的类树:
子类只列出一部分。
解决方案:
我们发现,所谓黑体、斜体等功能模块,事实上应当独立与对象本身而存在,而功能本身也应当互相独立,以便共同作用与对象。
上面的例子中,我们让功能Font继承自Text,使得我们在使用一个字体时,以下面的方式编写代码:
Font* = new ItalianFont();//创建一个斜体字对象
现在,我们尝试用对象组合的方式来解决这个问题。
class TextComponent{}
class SongText: public TextComponent{}
class YaheiText: public TextComponent{}
class Decorator: public TextComponent{
protected:
//重点!!
TextComponent* tc;
public:
Decorator(TextComponent* t): tc(t){}
virtual void operate() = 0;
}
class BlackFont: public Decorator{
public:
void operate(){...}
}//黑体
class ItalianFont: public Decorator{
public:
void operate(){...}
}//斜体
class HighlightFont: public Decorator{
public:
void operate(){...}
}//高亮
class UpperFont: public Decorator{
public:
void operate(){...}
}//上标
上面,我们抽象出了一个TextComponent抽象基类,而Decorator接口类和Text实际对象类都继承于它,这一步解除了对象和它的功能之间的继承关系。并且,我们这次将原本的Text对象分为了SongText和YaheiText两种,用于说明Decorator模式可以应用于多个同级对象。
接下来,将抽象基类的指针组合到Decorator类中,使得功能模块具有了一个抽象基类的指针属性。这一步看上去令人费解,而且,如何用这个机制实现我们想要的功能呢?我们再看下面的代码:
这里是对上面代码的调用
void process()
{
TextComonent* t1 = SongText();//创建宋体对象
BlackFont* t2 = new BlackFont(t1);
ItalianFont* t3 = new ItalianFont(t2);
HighlightFont* t4 = new HighlightFont(t3);
//发生了什么?
}
可以看到,在创建宋体对象后,我们把它作为参数创建了黑体功能对象,这时,调用t2的operate就合理地产生了一个黑体宋体。之后,我们又将t2组合到t3中,t3组合到t4中。经过这样的迭代,t4就拥有了宋体、黑体、斜体、高亮的全部属性!为什么?来看Decorate中operate函数的实现。
void operate()
{
tc->operate();
//下面是针对该功能的特定代码
...
}
现在一目了然了。在上面的迭代组合之后,调用t4->operate(),发生了下面的过程:
由于SongText的operate函数不会在调用其他的operate,调用终止,至此,每种功能对象包括字体对象本身的operate函数都得到了调用。
解释
通常,我们只会采用继承和组合两种策略中的一种。但在Decorator模式中,我们在TextComponent的子类Decorator中组合了一个TextComponent抽象基类的指针,就达到了上述Operate()函数在各个被组合的对象间反复横跳,最后完成了所有功能的奇幻效果。
Decorator模式是笔者认为最为巧妙的设计模式之一,因为它使用了发挥子类继承机制的优势,又利用组合功能避免了继承带来的硬编码不灵活和子类爆炸的问题。
这个模式的最深层原理,来自于八大设计原则中的单一职责原则。也就是说,相比于SongText, TaheiText这种字形,黑体、斜体这种扩展功能与前者的发展方向是不同的。一个类应该仅有一个引起它变化的原因。我们把字体装饰与字形两种发展方向分开编写,满足了这一原则,也就使得代码具有更高的灵活性和可扩展性。
当然,这基于对象本身在应用子功能时行为是类似的。如果对象应用子功能的变化无法预知(关系不稳定,也就是operate()函数无法稳定地描述对象应用功能时发生的行为),那么这个模式就不再适用。
Decorator模式也因此具有一个非常明显的特征:如果你看到一个类,它不仅继承于它的基类,同时也组合了它基类的指针,那么这极有可能就采用了Decorator模式。
总结
设计模式 | Decorator(装饰模式) |
---|---|
稳定点: | 对象与子功能的合作关系 |
变化点: | 发送者和接收者本身 |
效果: | 解决了继承带来的硬编码不灵活与子类爆炸的问题。 |
特点: | 类继承与类组合结合使用 |
类图:
2021.2.3 转载请标明出处