C++设计模式结构型模式———装饰模式

文章目录

一、引言

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

该模式展现出了运行时的一种扩展能力,以及比继承更强大和灵活的设计视角和设计能力,甚至在有些场合下,不使用该模式很难解决问题。


二、装饰器模式

装饰器模式也就是在原有行为之上进行拓展,并不会改变该行为。如在进行网络通信的时候,数据是基于TCP/IP四层模型进行传输的。通过下图可得知从应用层到物理层,数据每向下走一层就会被封装一层,最后将封装好的数据以比特流的方式发送给接收端,而且最重要的是,封装之后,数据只是变复杂了,并没有改变它是数据的本质。

我们继续使用之前的闯关游戏的例子,在游戏中,主角肯定需要许多界面。例如主角的背包,每个格子放一个为物品。下面我们以最简单的界面为例,列表控件。我们一步一步丰富该控件上的内容。先是一个普通的白板,其次加一个边框,然后再加一个垂直滚动条,最后加一个水平滚动条。

为了在游戏中绘制列表控件,传统的绘制方法可能采取以下步骤,来创建基础控件类:

  1. 首先,创建一个名为ListCtrl的基础类,它提供了draw方法来绘制基本的列表控件。创建一个名字叫作 BorderComponent的类,继承自ListCtrl类用于表示增加了边框的列表控件,提供draw方法绘制自身。

  2. 建一个名字叫作VerScBorderListCtrl的类,继承自BorderListCtrl,用于表示增加了边框又增加了垂直滚动条的列表控件,提供draw方法绘制自身。

  3. 创建一个名字叫作HorScVerScBorderListCtrl的类,继承自VerScBorderListCtrl,用于表示增加了边框又增加了垂直滚动条和水平滚动条的列表控件,提供draw方法绘制自身。

考虑下面两个问题:

  • 问题一 :如果需要给列表控件增加新内容,如阴影效果或外发光效果,我们不需要创建新的子类。相反,我们可以创建新的组件类(如ShadowComponentGlowEffectComponent),然后将这些组件添加到列表控件中。
  • 问题二 :如果我们想要创建一个没有边框但有垂直滚动条的列表控件,或者一个只有水平滚动条的列表控件,我们同样不需要创建更多的子类。我们可以根据需要组装相应的组件,例如,只添加VerticalScrollBarComponentListCtrl中,或者只添加HorizontalScrollBarComponent

上面这两个问题都会导致子类数量的泛滥,灵活性也非常差,所以,采用继承机制创建子类来解决列表控件的绘制显然不是一个好的解决方案,换一种思路,可以采用组装的方式来解决该问题。

  1. 先创建一个ListCtrl类代表普通列表控件(最基本的列表控件),提供draw方法绘制自身。
  2. 如果给这个普通的列表控件增加一个边框(看成是增加一种功能或者是一个装饰),则形成了一个带边框的列表控件。
  3. 同理,如果给这个普通的列表控件增加一个垂直滚动条,则形成了一个带垂直滚动条的列表控件,再给这个带垂直滚动条的列表控件增加一个水平滚动条,又形成了一个既带垂直滚动条又带水平滚动条的列表控件
  4. 总之,通过增加不同的装饰,可以生成不同的列表控件。

上述通过增加装饰来组装新列表控件的方式非常灵活,可以组装出各种各样的列表控件,例如将边框组装到普通列表控件上,就会形成带有边框的列表控件;再将水平滚动条组装到这个带有边框的列表控件上,就会立即形成带有边框和水平滚动条的列表控件。这种通过组装方式将一个类的功能不断增强的思想(动态的增加新功能),就是装饰模式核心的设计思想。

组装特性:而不是通过继承来添加新特性,我们采用组装的方式。这意味着我们可以创建独立的组件类,如用于表示不同的特性。

首先我们创建一个控件类

c++ 复制代码
class Control {
public:
	virtual void draw() = 0;	//用于把自身绘制到屏幕上
	virtual ~Control(){} 
};

上面提到的列表控件ListCtrl以及其他控件,例如文本控件TextCtrl等,应该作为抽象控件的子类,也就是作为具体控件,这里以列表控件作为具体控件的代表,创建
ListCtrl类:

c++ 复制代码
// 列表控件类
class ListCtrl : public Control {
public:
	virtual void draw()
	{
		cout << "绘制普通的列表控件!" << endl;//具体可以用Directx或OpenGL来绘制 
	}
};

接下来我们定义一个装饰器,这是一个抽象的装饰器类(用于作具体装饰器的父类):

c++ 复制代码
// 抽象的装饰器类
class Decorator : public Control {
public:
    Decorator(shared_ptr<Control> tmpctrl) : m_control(tmpctrl) {} // 构造函数

    virtual void draw() override {
        m_control->draw(); // 调用被装饰控件的draw方法
    }

protected:
    shared_ptr<Control> m_control; // 用智能指针管理需要被装饰的控件
};

抽象装饰器类(Decorator类)的父类依旧是Control类,这可能会造成一些理解上的困扰。试想,一个普通的列表控件经过装饰器装饰后生成一个新的列表控件(例如带边框的列表控件),该列表控件仍然要绘制自己,所以要有draw方法,因此,这个抽象的装饰器类会继承自Control。从另外一个角度来理解,根据public继承的is-a特性,经过装饰器装饰过的列表控件依旧是列表控件(不要把装饰器单纯理解成装饰器,而是理解成经过包装之后的新控件),所以抽象装饰器类继承自Control类也合乎情理。

Decorator类中有一个m_control成员变量,其类型是Control类型的指针,Control同时作为Decorator类的父类,所以从这个角度来讲,Decorator类与Control类又是一种组合关系。

构造函数的形参也是Control类型的指针,代表的当然是被装饰的控件。虚函数draw中的m_control>draw()调用的是哪个类的draw取决于m_control指向的是哪个对象。

下面给出边框、滚动条等具体装饰器:

c++ 复制代码
// 具体的"边框"装饰器类
class BorderDec : public Decorator {
public:
    BorderDec(shared_ptr<Control> tmpctrl) : Decorator(tmpctrl) {} // 构造函数

    virtual void draw() override {
        Decorator::draw(); // 调用父类的draw方法
        drawBorder(); // 绘制边框
    }

private:
    void drawBorder() {
        cout << "绘制边框!" << endl;
    }
};

// 具体的"垂直滚动条"装饰器类
class VerScrollBarDec : public Decorator {
public:
    VerScrollBarDec(shared_ptr<Control> tmpctrl) : Decorator(tmpctrl) {} // 构造函数

    virtual void draw() override {
        Decorator::draw(); // 调用父类的draw方法
        drawVerScrollBar(); // 绘制垂直滚动条
    }

private:
    void drawVerScrollBar() {
        cout << "绘制垂直滚动条!" << endl;
    }
};

// 具体的"水平滚动条"装饰器类
class HorScrollBarDec : public Decorator {
public:
    HorScrollBarDec(shared_ptr<Control> tmpctrl) : Decorator(tmpctrl) {} // 构造函数

    virtual void draw() override {
        Decorator::draw(); // 调用父类的draw方法
        drawHorScrollBar(); // 绘制水平滚动条
    }

private:
    void drawHorScrollBar() {
        cout << "绘制水平滚动条!" << endl;
    }
};

给出如下案例:

c++ 复制代码
// 创建基础控件实例
shared_ptr<Control> myControl = make_shared<ListCtrl>();
// 使用装饰器组合功能
shared_ptr<Control> borderedControl = make_shared<BorderDec>(myControl);
shared_ptr<Control> verScrollControl = make_shared<VerScrollBarDec>(borderedControl);
shared_ptr<Control> horScrollControl = make_shared<HorScrollBarDec>(verScrollControl);
// 绘制最终控件
horScrollControl->draw();
/*
绘制普通的列表控件!
绘制边框!
绘制垂直滚动条!
绘制水平滚动条!
*/

ControlDecorator是继承关系也是组合关系。

Decorator类这边,表示Decorator类中包含Control类的对象指针(m_control)作为成员变量。ListCtrl代表着列表控件这个主体类,而其他继承自Decorator的子类都是装饰器类。

引人"装饰"设计模式的定义(实现意图):动态地给一个对象添加一些额外的职责。就增加功能来说,该模式相比生成子类更加灵活。

装饰模式包含4种角色。

  • 抽象构建Control ):具体构件ListCtrl和抽象装饰器类Decorator的共同父类,其中定义了必需的接口(draw),用来实现必需的业务。其引人的目的是让调用者以一致的方式处理未被修饰的对象以及经过修饰之后的对象,实现客户端的透明操作。
  • 具体构建ListCtrl ):抽象构件Control的子类,定义具体的构件,实现抽象构件中定义的接口,此后,装饰器就可以给该构件增加额外的方法(职责)。
  • 抽象装饰器类Decorator ):抽象构件Control的子类,在其中定义了一个与Control接口一致的接口(draw),子类通过对该接口的扩展,达到装饰的目的。
  • 具体装饰器类BorderDecade、VerScrollBarDec、HorScrollBarDec ):作为抽象装饰器类的子类。每个具体装饰器类都增加了一些新的方法(例如drawBorderdrawVerScrollBardrawHorScrollBar等)来修饰该构件或者说扩充该构件的能力,之后通过对draw接口的扩展,来达到最终的修饰目的。

装饰器模式结构

穿衣服也是是使用装饰的一个例子。 觉得冷时, 你可以穿一件毛衣。 如果穿毛衣还觉得冷, 你可以再套上一件夹克。 如果遇到下雨, 你还可以再穿一件雨衣。 所有这些衣物都 "扩展" 了你的基本行为, 但它们并不是你的一部分, 如果你不再需要某件衣物, 可以方便地随时脱掉。


三、总结

对装饰器模式的总结如下:

  1. 灵活性与可扩展性:装饰器模式避免了传统继承方式导致的子类膨胀问题,使得项目设计更加灵活和可扩展。它提供了一种运行时扩展能力,允许将新功能动态地附加到现有对象上。相较于继承,装饰器模式展现了更强的设计能力和视角,可以视为对继承的更优替代方案。新增的装饰器类能够为对象增加新的职责或能力,符合开闭原则。但使用开闭原则时需谨慎,应该集中在最可能改变的地方,避免滥用和复杂性增加。

  2. 相同父类的优势 :装饰器对象与被装饰对象共享相同的父类,使得装饰器可以替代被装饰对象,并支持多个装饰器对同一对象进行包装。这种设计方式确保了 draw 接口的正确继承,同时通过组合为对象增添新的能力。

  3. 小对象的问题 :装饰器模式的一个主要缺点是可能导致生成大量小对象。在 main 函数中,plistctrl_b_v 依赖于 plistctrl_bplistctrl。这些小对象之间的差异通常不大,成员变量和成员函数相似度高,过多的小对象会占用资源并影响程序性能,管理起来也相对复杂。例如,为了执行 plistctrl_b_v->draw();,需要同时确保 plistctrl_b_vplistctrl_bplistctrl 对象的有效性。如果在执行过程中不慎删除了某个依赖对象,可能导致程序崩溃。因此,程序员在编写代码时必须小心谨慎,以避免难以排查的错误。

适配器模式可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

责任链模和装饰模式的类结构非常相似。 两者都依赖递归组合将需要执行的操作传递给一系列对象。 但是, 两者有几点重要的不同之处。责任链的管理者可以相互独立地执行一切操作, 还可以随时停止传递请求。 另一方面, 各种装饰可以在遵循基本接口的情况下扩展对象的行为。 此外, 装饰无法中断请求的传递。

组合模式和装饰的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。

装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 "求和"。可以使用装饰来扩展组合树中特定对象的行为。大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 你可以通过该模式来复制复杂结构, 而非从零开始重新构造。装饰可更改对象的外表, 策略模式则让你能够改变其本质。

装饰和代理有着相似的结构, 但是其意图却非常不同。 这两个模式的构建都基于组合原则, 也就是说一个对象应该将部分工作委派给另一个对象。 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期, 而装饰的生成则总是由客户端进行控制。

相关推荐
SSL_lwz23 分钟前
P11290 【MX-S6-T2】「KDOI-11」飞船
c++·学习·算法·动态规划
熬夜学编程的小王30 分钟前
【C++篇】从基础到进阶:全面掌握C++ List容器的使用
开发语言·c++·list·双向链表·迭代器失效
悄悄敲敲敲32 分钟前
C++:智能指针
开发语言·c++
zhangpz_35 分钟前
c ++零基础可视化——vector
c++·算法
菜菜-plus1 小时前
java 设计模式 模板方法模式
java·设计模式·模板方法模式
萨达大1 小时前
23种设计模式-模板方法(Template Method)设计模式
java·c++·设计模式·软考·模板方法模式·软件设计师·行为型设计模式
刀鋒偏冷1 小时前
ninja: error: ‘/opt/homebrew/Cellar/opensslxxx/xx/lib/libssl.dylib
c++
理论最高的吻1 小时前
98. 验证二叉搜索树【 力扣(LeetCode) 】
数据结构·c++·算法·leetcode·职场和发展·二叉树·c
沈小农学编程1 小时前
【LeetCode面试150】——202快乐数
c++·python·算法·leetcode·面试·职场和发展
ZZZ_O^O2 小时前
【动态规划-卡特兰数——96.不同的二叉搜索树】
c++·学习·算法·leetcode·动态规划