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. 总之,通过增加不同的装饰,可以生成不同的列表控件。

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

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

首先我们创建一个控件类

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

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

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

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

// 抽象的装饰器类
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指向的是哪个对象。

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

// 具体的"边框"装饰器类
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;}
};

给出如下案例:

// 创建基础控件实例
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 对象的有效性。如果在执行过程中不慎删除了某个依赖对象,可能导致程序崩溃。因此,程序员在编写代码时必须小心谨慎,以避免难以排查的错误。

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/59083.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

「iOS」——知乎日报一二周总结

知乎日报仿写 前言效果Manager封装网络请求线程冲突问题下拉刷新添加网络请求的图片通过时间戳和日期格式化获取时间 总结 前言 前两周内容的仿写&#xff0c;主要完成了首页的仿写&#xff0c;进度稍慢。 效果 Manager封装网络请求 知乎日报的仿写需要频繁的申请网络请求&am…

Profinet、Ethernet/IP 工业以太网无线通信解决方案

在工业现场&#xff0c;我们常常会面临这样的困扰&#xff1a;两个PLC之间、PLC 跟远程IO之间或者PLC 跟伺服之间由于种种原因不方便布线&#xff0c;严重影响了通讯效率和生产进程。为了解决这一难题&#xff0c;三格电子设计了一款工业以太网无线网桥&#xff0c;这款无线网桥…

核心概念解析Caffeine 缓存模型与策略

1. 简介 什么是 Caffeine Caffeine 是一个高性能的 Java 缓存库&#xff0c;专为提高内存缓存的效率和灵活性而设计。它由 Google 的 Guava Cache 项目启发&#xff0c;并提供了更高的性能和更丰富的功能集。Caffeine 以其卓越的缓存命中率和内存管理能力而广受欢迎&#xff…

GaussDB Ustore存储引擎解读

GaussDB Ustore存储引擎解读 GaussDB是华为云推出的一款高性能数据库产品&#xff0c;其内核新增的Ustore存储引擎为企业级用户提供了更高性能的数据库服务。Ustore存储引擎&#xff0c;又名In-place Update存储引擎&#xff08;原地更新&#xff09;&#xff0c;是GaussDB内核…

【数据结构】二叉树——前中后序遍历

一、如何遍历二叉树 以图上这个二叉树作为例子&#xff0c;我们若想要访问二叉树中每一个元素 我们一般是采用递归的方式 比如我们要访问完整个二叉树&#xff0c;我们进行递归先访问根的左子树&#xff0c;然后因为递归再次调用&#xff0c;我们会先一直访问二叉树左子树&…

桑基图在医学数据分析中的更复杂应用示例

桑基图&#xff08;Sankey Diagram&#xff09;能够有效地展示复杂的流动关系&#xff0c;特别适合用于医学数据分析中的多种转归和治疗路径的可视化。接下来&#xff0c;我们将构建一个稍微复杂的示例&#xff0c;展示不同疾病患者在治疗过程中的流动&#xff0c;以及他们的治…

面试问题:hash和history的区别

问&#xff1a; hash和history的区别 回答&#xff1a; ‌工作原理‌&#xff1a; ‌hash模式‌&#xff1a;利用锚点技术&#xff0c;通过改变URL中的hash部分&#xff08;即#后面的部分&#xff09;来实现页面跳转&#xff0c;不会重新加载页面。例如&#xff0c;URL变为ht…

WPS 表格奇数行或偶数行填充颜色(Excel也适用)

今天在做项目测试用例&#xff0c;全都是白色底纹&#xff0c;日以继夜的赶项目 大家都头脑不清晰了&#xff0c;看着容易错行&#xff0c;看我一键更新 如本次测试用例&#xff0c;单元格区域的奇数行填充灰色。 点击单元格最左上角选定区域&#xff0c;然后依次点击【开始】…

RT-DETR:替代YOLO的更快实时对象检测模型(附代码)

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

JavaScript。—关于语法基础的理解—

一、程序控制语句 JavaScript 提供了 if 、if else 和 switch 3种条件语句&#xff0c;条件语句也可以嵌套。 &#xff08;一&#xff09;、条件语句 1、单向判断 &#xff1a; if... &#xff08;1&#xff09;概述 < if >元素用于在判断该语句是否满足特定条…

django各个文件简单介绍

templates 存放前端的模板文件 manage.py django的命令行工具 venv 这个目录是虚拟环境的相关文件 __pycache__ 编译后的文件夹&#xff0c;不执行则无 Include: python 编译器的C语言头文件源码 Lib: python的标准库&#xff0c;库都安装在这里面的site-packages文件夹里 Sc…

npm入门教程6:npm脚本

一、npm脚本的基本用法 定义脚本 在package.json文件的scripts字段中&#xff0c;你可以定义多个脚本命令。每个脚本都是一个键值对&#xff0c;其中键是脚本的名称&#xff0c;值是要执行的命令。例如&#xff1a; "scripts": {"start": "node index…

ComfyUI - ComfyUI 工作流中集成 SAM2 + GroundingDINO 处理图像与视频 教程

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/143359538 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 SAM2 与…

Android13预置应用及授权开发

在android13中&#xff0c;要预置一个对讲应用&#xff0c;从预置和授权&#xff0c;梳理了一下&#xff0c;以便后续查询使用。在此记录 一放置应用 我的apk应用放在vendor下面&#xff0c; 路径&#xff1a;projectroot/vendor/fly/package/apps/DMR/flydmr.apk (vendor/fl…

英语写作中“出于……”out of的用法

out of &#xff08;出于……&#xff09;是我们容易忽略但比较实用的表达。“出于……的考虑”、“出于……担忧”等在英语写作中用out of表达&#xff1a; out of consideration/concern/responsibility/respect/…… 例如&#xff1a; The service provider deploys mult…

【密码学】全同态加密基于多项式环计算的图解

全同态加密方案提供了一种惊人的能力 —— 能够在不知道数据具体内容的情况下对数据进行计算。这使得你可以在保持潜在敏感源数据私密的同时&#xff0c;得出问题的答案。 这篇文章的整体结构包括多项式环相关的数学介绍&#xff0c;基于多项式环的加密和解密是如何工作的&…

[java][框架]springMVC(1/2)

目标 知道SpringMVC的优点编写SpringMVC入门案例使用PostMan发送请求掌握普通类型参数传递掌握POJO类型参数传递掌握json数据参数传递掌握响应json数据掌握rest风格快速开发 一、SpringMVC简介 1 SpringMVC概述 问题导入 SpringMVC框架有什么优点&#xff1f; 1.1 Spring…

基于STM32健康监控系统/智能手环/老人健康检测系统/心率血氧血压

基于STM32健康监控系统/智能手环/老人健康检测系统/心率血氧血压 持续更新&#xff0c;欢迎关注!!! 基于STM32健康监控系统/智能手环/老人健康检测系统/心率血氧血压 随着人民生活质量的提高和生活节奏的加快&#xff0c;人体健康监测成为全球关注的焦点之一。基于物联网的人体…

百度文心智能体:巧用汉字笔画生成与汉字搜索插件,打造一个学习汉字的教育类智能体

这篇文章&#xff0c;主要介绍如何巧用汉字笔画生成与汉字搜索插件&#xff0c;打造一个学习汉字的教育类智能体。 目录 一、教育类智能体 1.1、智能体演示 1.2、智能体插件 1.3、智能体prompt &#xff08;1&#xff09;角色和目标 &#xff08;2&#xff09;思考路径 …

质数筛c++

题目描述 输入 nn 个不大于 10^510^5 的正整数。要求全部储存在数组中&#xff0c;去除掉不是质数的数字&#xff0c;依次输出剩余的质数。 输入格式 第一行输入一个正整数 nn&#xff0c;表示整数个数。 第二行输入 nn 个正整数 aiai​&#xff0c;以空格隔开。 输出格式…