设计模式详解:Decorator(装饰模式)

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两个类的代码组合而已,实现起来必然也只是复制粘贴。

最后,我们来看一下上面这种写法的类树:

Text Font virtual operate BlackFont operate() ItalianFont operate() BlackItalianFont operate() HighlightFont operate()

子类只列出一部分

解决方案:

我们发现,所谓黑体、斜体等功能模块,事实上应当独立与对象本身而存在,而功能本身也应当互相独立,以便共同作用与对象。

上面的例子中,我们让功能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(),发生了下面的过程:

调用t3->operate()
调用t2->operate()
调用t1->operate()
回到t2->operate()
回到t3->operate()
回到t4->operate()
t4
t3
t2
t1

由于SongText的operate函数不会在调用其他的operate,调用终止,至此,每种功能对象包括字体对象本身的operate函数都得到了调用。

解释

通常,我们只会采用继承和组合两种策略中的一种。但在Decorator模式中,我们在TextComponent的子类Decorator中组合了一个TextComponent抽象基类的指针,就达到了上述Operate()函数在各个被组合的对象间反复横跳,最后完成了所有功能的奇幻效果。

Decorator模式是笔者认为最为巧妙的设计模式之一,因为它使用了发挥子类继承机制的优势,又利用组合功能避免了继承带来的硬编码不灵活和子类爆炸的问题

这个模式的最深层原理,来自于八大设计原则中的单一职责原则。也就是说,相比于SongText, TaheiText这种字形,黑体、斜体这种扩展功能与前者的发展方向是不同的。一个类应该仅有一个引起它变化的原因。我们把字体装饰与字形两种发展方向分开编写,满足了这一原则,也就使得代码具有更高的灵活性和可扩展性。

当然,这基于对象本身在应用子功能时行为是类似的。如果对象应用子功能的变化无法预知(关系不稳定,也就是operate()函数无法稳定地描述对象应用功能时发生的行为),那么这个模式就不再适用。

Decorator模式也因此具有一个非常明显的特征:如果你看到一个类,它不仅继承于它的基类,同时也组合了它基类的指针,那么这极有可能就采用了Decorator模式。

总结

设计模式 Decorator(装饰模式)
稳定点: 对象与子功能的合作关系
变化点: 发送者和接收者本身
效果: 解决了继承带来的硬编码不灵活与子类爆炸的问题。
特点: 类继承与类组合结合使用

类图:

Component operate() ConcreteComponentA operate() ConcreteComponentB operate() Decorator Component* cp operate() ConcreteDecoratorA Component* cp operate() ConcreteDecoratorB Component* cp operate()

2021.2.3 转载请标明出处

猜你喜欢

转载自blog.csdn.net/natrick/article/details/113629250