C++设计模式结构型模式———组合模式

文章目录

  • 一、引言
  • 二、组合模式
  • 三、总结

一、引言

组合模式是一种结构型设计模式, 可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。代码实现中涉及了递归调用。组合模式与传统上的“类与类之间的组合关系”没有关联,不要混为一谈。

组合模式主要用来处理树形结构的数据,例如Windows或者类UNIX操作系统中文件的组织方式就是典型的树形结构。这里所指的数据就是这些文件或者文件夹,处理树形结构数据是指例如可以对它们进行遍历以显示目录或文件名(查看目录文件结构)、进行某些动作(例如信息统计、文件杀毒)等操作。


二、组合模式

组合模式主要是用来表达和处理树形结构数据的,作为树形结构的数据,显然要有一个树根,树根下面可以有树枝和树叶两种节点,而树枝下面又可能进一步生长出新的树枝和叶(树叶属于末端节点,其上不会生长出任何其他内容),以此类推。

例如操作系统的文件系统:

在这里插入图片描述

看一看如何用程序来把这个目录层次结构组织并输出(绘制出来),输出的结果类似于
用tree命令显示root目录产生的结果(考虑到组合模式不太好理解,可以先抛开这个模
式),这个范例的难点在于目录中还会包含更深层次的目录和文件,而这些目录和文件的名字都要求输出出来,所以实现思路应该涉及递归编程。首先创建一个用于表示文件的类FileDir,代码如下,注意代码中的注释:

// 文件相关类
class File {
public:File(const string& name) : m_sname(name) {}void ShowName(const string& lvlstr) const { // lvlstr:为了显示层次关系的缩进字符串内容cout << lvlstr << "-" << m_sname << endl;// 显示”-”代表是一个文件,属末端节点(不会再有子节点)}private:string m_sname; // 文件名
};// 目录
class Dir {
public:Dir(const string& name) : m_sname(name) {}// 目录中可以增加其他文件void AddFile(shared_ptr<File> pfile) {m_childFiles.push_back(pfile);}// 目录中可以增加其他目录void AddDir(shared_ptr<Dir> pdir) {m_childDirs.push_back(pdir);}// 显示目录名,同时也负责其下面的文件和目录名的显示工作void ShowName(const string& lvlstr) const {// (1) 输出本目录名cout << lvlstr << "+" << m_sname << endl; // 显示” + "代表是一个目录,其中会包含其他内容// (2) 输出所包含的文件名string newLvlStr = lvlstr + "   ";for (const auto& file : m_childFiles) {file->ShowName(newLvlStr + "  "); // 本目录中的文件和目录的显示,要缩进一些来显示}// (3) 输出所包含的目录名for (const auto& dir : m_childDirs) {dir->ShowName(newLvlStr + "  "); // 显示目录名,这里涉及了递归调用}}private:string m_sname; // 目录名list<shared_ptr<File>> m_childFiles; // 目录中包含的文件列表list<shared_ptr<Dir>> m_childDirs; // 目录中包含的子目录列表
};

我们给个案例使用该函数

// 创建文件
auto file1 = make_shared<File>("file1.txt");
auto file2 = make_shared<File>("file2.txt");
auto file3 = make_shared<File>("file3.txt");// 创建目录
auto dir1 = make_shared<Dir>("dir1");
auto dir2 = make_shared<Dir>("dir2");
auto dir3 = make_shared<Dir>("dir3");// 组装目录结构
dir1->AddFile(file1.get());
dir1->AddDir(dir2.get());
dir2->AddFile(file2.get());
dir2->AddDir(dir3.get());
dir3->AddFile(file3.get());// 显示目录结构
dir1->ShowName(""); // 从根目录开始显示/*输出如下
+dir1-file1.txt+dir2-file2.txt+dir3-file3.txt
*/

以一个树根为起点,可以遍历(访问)到所有该根下的树节点(既包含树枝,又包含树叶)。在本范例中,File类和Dir类的ShowName函数虽然名字相同,但它们做的事情并不相同,因为Dir类的ShowName不但要显示自身的名字,还要显示其下的文件和目录名字,而其下目录名字的显示,使用的正是递归调用,当然这里所说的递归区别于传统意义上的递归(函数调用自身),而是一种针对对象本身的递归。

上面这个范例代码中存在的问题是:为了区分文件和目录,分别创建了FileDir两个类,这种区分比较多余,为此,引人了组合模式,该模式专门针对以树形结构的形式组织对象时,不再将FileDir类单独分开,而是引人一个新的抽象类(例如FileSystem)并提供公共的接口(成员函数),而后让FileDir类分别继承自FileSystem类。看一看如何采用组合模式改造上述范例代码。


class FileSystem {
public:virtual void ShowName(int level) const = 0;virtual int Add(shared_ptr<FileSystem> pfilesys) = 0;virtual int Remove(shared_ptr<FileSystem> pfilesys) = 0;virtual ~FileSystem() {}
};// 文件相关类
class File : public FileSystem {
public:File(string name) : m_sname(name) {}virtual void ShowName(int level) const override {for (int i = 0; i < level; ++i) cout << "    ";cout << "-" << m_sname << endl;}virtual int Add(shared_ptr<FileSystem> pfilesys) override {return -1; // 文件不能添加子文件或子目录}virtual int Remove(shared_ptr<FileSystem> pfilesys) override {return -1; // 文件不能移除子文件或子目录}private:string m_sname; // 文件名
};// 目录
class Dir : public FileSystem {
public:Dir(const string& name) : m_sname(name) {}virtual void ShowName(int level) const override {// (1) 显示若干空格用于对齐for (int i = 0; i < level; ++i) cout << "    ";// (2) 输出本目录名cout << "+" << m_sname << endl;// (3) 显示的层级向下走一级level++;// (4) 输出所包含的子内容(可能是文件,也可能是子目录)for (const auto& child : m_child) {child->ShowName(level); // 显示子内容}}virtual int Add(shared_ptr<FileSystem> pfilesys) override {m_child.push_back(pfilesys);return 0;}virtual int Remove(shared_ptr<FileSystem> pfilesys) override {m_child.remove(pfilesys);return 0;}private:string m_sname; // 目录名list<shared_ptr<FileSystem>> m_child; // 目录中包含的文件和子目录
};

给一个使用案例:

// 创建文件和目录
auto root = make_shared<Dir>("root");
auto dir1 = make_shared<Dir>("dir1");
auto dir2 = make_shared<Dir>("dir2");
auto file1 = make_shared<File>("file1.txt");
auto file2 = make_shared<File>("file2.txt");
auto file3 = make_shared<File>("file3.txt");// 构建文件结构
root->Add(file1);
root->Add(dir1);
dir1->Add(file2);
dir1->Add(dir2);
dir2->Add(file3);// 显示整个文件结构
root->ShowName(0);// 移除文件和目录
dir1->Remove(file2); // 从 dir1 中移除 file2
cout << "\nAfter removing file2.txt:\n";
root->ShowName(0) ;
/* 案例输出结果
+root-file1.txt+dir1-file2.txt+dir2-file3.txtAfter removing file2.txt:
+root-file1.txt+dir1+dir2-file3.txt
*/

树形结构是一种广泛应用的数据结构,它在多种场景中都有体现,例如:

  1. 在操作系统中,文件系统的目录结构就是一个树形结构;
  2. 在各种软件工具中,菜单的层级关系也构成了一个树形结构;
  3. 在办公软件中,公司的组织架构,包括公司下的多个部门以及分公司及其部门,形成了一个树形组织结构;
  4. 在窗口应用程序中,主窗口与其包含的子窗口以及其他控件共同构成了一个树形结构;
  5. 在编程时,TreeCtrl和TreeViewUI等控件也是树形结构的实例。

组合模式非常适合处理这种树形结构,它允许通过简单的代码实现,例如执行pdir1->ShowName(0);,就能够遍历整个树形结构,并通过递归调用来一致性地处理树中的所有节点。这里的例子展示了无论节点是树枝(包含其他节点的节点)还是树叶(没有子节点的节点),都可以调用ShowName成员函数,这就是组合模式一致性处理树形结构的一个体现。

引人组合设计模式的定义:将一组对象(如文件和目录)组织成树形结构以表示“部分-整体”的层次结构(如目录中包含文件和子目录)。使得用户对单个对象(文件)和组合对象(目录)的操作/使用/处理(递归遍历并执行ShowName逻辑等)具有一致性。

总之,组合模式之所以称为结构型模式,是因为该模式提供了一个结构,可以同时包容单个对象和组合对象。组合模式发挥作用的前提是具体数据必须能以树形结构的方式表示,树中包含了单个对象和组合对象。该模式专注于树形结构中单个对象和组合对象的递归遍历(只有递归遍历才能体现出组合模式的价值),能把相同的操作(FileSystem定义的接口)应用在单个以及组合对象上,并且可以忽略单个对象和组合对象之间的差别。从模式命名上,笔者认为命名成组合模式其实并不太恰当,命名成树形模式似乎更好。

在这里插入图片描述

组合模式的一般包含3种角色。

  1. 抽象组件Component):为树枝和树叶定义接口(例如,增加、删除、获取子节点等),可以是抽象类,包含所有子类公共行为的声明或默认实现体。这里指FileSystem类。
  2. 叶子组件Leaf):用于表示树叶节点对象,这种对象没有子节点,因此抽象组件中定义的一些接口(例如Add、Remove)实际在这里没有实现的意义。这里指File类。
    • 这种叶子组件(类)对于组合模式可能不止一个,例如,若对某个目录进行杀毒,可以在抽象组件中提供KillVirus成员函数,类似ShowName,而后可以定义若干个不同的叶子类,例如定义ExeFile类并实现KillVirus专门灭杀可执行文件中的病毒,定义ImgFile类并实现KillVirus专门灭杀图像文件中的病毒等。
  3. 树枝组件Composite):用于表示一个容器(树枝)节点对象,可以包含子节点,子节点可以是树叶,也可以是树枝,其中提供了一个集合用于存储子节点(以此形成一个树形结构,可以通过递归来访问所有节点)。实现了抽象组件中定义的接口。这里指Dir类。
    • Dir类中提供的集合是一个用于存储子节点的list容器,当然用其他容器保存子节点也完全可以。

组合模式结构

在这里插入图片描述


三、总结

组合模式的主要优点包括:

  1. 客户端一致性处理:组合模式允许客户端以相同的方式对待单个对象和组合对象,无需关心它们在层次结构中的位置,从而简化了客户端代码的编写。
  2. 易于扩展:无论是添加新的叶子组件还是树枝组件,都只需添加一个新的继承自抽象组件的类,这符合开闭原则,即对扩展开放,对修改关闭。
  3. 灵活的树形结构实现:组合模式为树形结构的面向对象实现提供了一种灵活的方法,通过递归遍历单个对象和组合对象,可以处理复杂的树形结构。

在使用组合模式时,需要注意以下问题:

  1. 抽象组件的设计:为了使客户端能够一致地使用组件,抽象组件应该定义尽可能多的公共操作,并为这些操作提供默认实现。叶子组件和树枝组件可以根据需要重写这些操作。
  2. 父节点指针:根据具体业务需求,组件可能需要包含一个指向父节点的指针,这有助于在遍历节点或执行删除操作时更加方便。
  3. 遍历顺序和管理:在某些场景下,如语法分析树的表示,需要考虑节点的遍历顺序。这可能需要在添加和删除子节点时进行更复杂的管理,可能需要修改相关类的代码。

与遍历顺序相关的,还有子节点的存储问题。在示例中使用的是list这种顺序容器,但也可以根据实际情况选择其他顺序容器。C++标准库还提供了关联容器和无序容器,可以根据使用便利性和访问效率来选择合适的容器。

桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。

可以在创建复杂组合树时使用生成器模式, 因为这可使其构造步骤以递归的方式运行。

责任链模式通常和组合模式结合使用。 在这种情况下, 叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。

可以使用迭代器模式来遍历组合树。也可以使用访问者模式对整个组合树执行操作。当然,使用享元模式实现组合树的共享叶节点以节省内存。组合和装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。

装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。

大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。 可以通过该模式来复制复杂结构, 而非从零开始重新构造。

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

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

相关文章

『大模型笔记』IBM技术团队:什么是智能体型RAG!

『大模型笔记』IBM技术团队:什么是智能体型RAG! 文章目录 一. 『大模型笔记』IBM技术团队:什么是智能体型RAG!二. 参考文献一. 『大模型笔记』IBM技术团队:什么是智能体型RAG! ✅检索增强生成(RAG)是一种结合检索和生成能力的技术,通过从向量数据库检索相关信息作为上…

快速傅里叶变换(FFT)基础(附python实现)

对于非专业人士&#xff0c;傅里叶变换一直是一个神秘的武器&#xff0c;它可以分析出不同频域的信息&#xff0c;从时域转换到频域&#xff0c;揭示了信号的频率成分&#xff0c;对于数字信号处理&#xff08;DSP&#xff09;、图像、语音等数据来说&#xff0c;傅里叶变换是最…

丹摩征文活动|新手入门指南

在AI大模型发展的今天&#xff0c;高性能计算平台已经成为研究和应用领域中不可或缺的工具。丹摩智算平台专注于为用户提供强大的算力支持和便捷的操作流程&#xff0c;帮助研究者和开发者更高效地训练和优化AI模型。本教程将深入介绍丹摩智算平台的核心功能及具体操作步骤&…

Java项目实战II基于Spring Boot的便利店信息管理系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。获取源码联系方式请查看文末 一、前言 在快节奏的…

【VScode】VScode内的ChatGPT插件——CodeMoss全解析与实用教程

在当今快速发展的编程世界中&#xff0c;开发者们面临着越来越多的挑战。如何提高编程效率&#xff0c;如何快速获取解决方案&#xff0c;成为了每位开发者心中的疑问。今天&#xff0c;我们将深入探讨一款颠覆传统编程体验的插件——CodeMoss&#xff0c;它将ChatGPT的强大功能…

数据冒险-dadd,sub和and

从图中的流水线执行情况来看&#xff0c;我们可以分析指令之间的依赖关系。图中每条指令对应的执行阶段标注为 IF (取指令)&#xff0c;ID (指令译码)&#xff0c;EX (执行)&#xff0c;Mem (访存)&#xff0c;和 WB (写回)。以下是对每条指令依赖情况的分析&#xff1a; 第一条…

如何修改WordPress经典编辑器的默认高度?

boke112百科有一个使用WordPress搭建的小网站&#xff0c;文章内容就是几个字不到一行&#xff0c;但是每次使用经典编辑器编辑文章时&#xff0c;都觉得编辑器默认高度太高了&#xff0c;影响了我添加文章摘要和其他属性&#xff0c;有没有办法修改WordPress经典编辑器的默认高…

量化研究--年化57%全球动量模型策略回测,学习使用

文章声明:本内容为个人的业余研究&#xff0c;和任何单位&#xff0c;机构没有关系&#xff0c;文章出现的股票代码&#xff0c;全部只是测试例子&#xff0c;不做投资参考&#xff0c;投资有风险&#xff0c;代码学习使用&#xff0c;不做商业用途 本文利用全球动量模型策略回…

【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期

目录 1. start() (1) start() 的性质 (2) start() 和 Thread类 的关系 2. 终止一个线程 (1)通过共享的标记结束线程 1. 通过共享的标记结束线程 2. 关于 lamda 表达式的“变量捕获” (2) 调用interrupt()方法 1. isInterrupted() 2. currentThread() …

Linux驱动开发(3):字符设备驱动

上一章节我们了解到什么是内核模块&#xff0c;模块的加载卸载详细过程以及内核模块的使用等内容。 本章&#xff0c;我们将学习驱动相关的概念&#xff0c;理解字符设备驱动程序的基本框架&#xff0c;并从源码上分析字符设备驱动实现和管理。 主要内容有如下五点&#xff1a;…

布谷直播源码部署服务器关于数据库配置的详细说明

布谷直播源码搭建部署配置接口数据库 /public/db.php&#xff08;2019年8月后的系统在该路径下配置数据库&#xff0c;老版本继续走下面的操作&#xff09; 在项目代码中执行命令安装依赖库&#xff08;⚠️注意&#xff1a;如果已经有了vendor内的依赖文件的就不用执行了&am…

【Linux】从零开始使用多路转接IO --- 理解EPOLL的 LT水平触发模式 与 ET边缘触发模式

当你偶尔发现语言变得无力时&#xff0c; 不妨安静下来&#xff0c; 让沉默替你发声。 --- 里则林 --- 从零开始认识多路转接 1 EPOLL优缺点2 EPOLL工作模式 1 EPOLL优缺点 poll 的优点(和 select 的缺点对应) 接口使用方便&#xff1a;虽然拆分成了三个函数&#xff0c;…

云轴科技ZStack助力新远科技开启化工行业智能制造新篇章

新远科技基于云轴科技ZStack Cube超融合和ZStack Zaku容器云平台打造了灵活高效的IT基础设施&#xff0c;实现了IaaS和PaaS层的全面覆盖&#xff0c;优化了资源利用率&#xff0c;降低了硬件成本和运维复杂性&#xff0c;同时强化了数据安全和业务连续性。 化工行业的数字化先…

认识类和对象

认识类 类是用来对一个实体 ( 对象 ) 来进行描述的&#xff0c;主要描述该实体(对象)具有哪些属性(外观尺寸等)&#xff0c;哪些功能(用来干啥) 类中包含的内容称为 类的成员。属性主要是用来描述类的&#xff0c;称之为 类的成员属性或者 类成员变量。方法主要说明类具有哪些功…

npm镜像的常用操作

查看当前配置的 npm 镜像 npm config get registry切换官方镜像 npm config set registry https://registry.npmjs.org/切换淘宝镜像(推荐) npm config set registry https://registry.npmmirror.com/切换腾讯云镜像 npm config set registry http://mirrors.cloud.tencent…

网购选择困难症怎么破?别忘了你的这位“帮手”

每年双十一对不少人来说&#xff0c;既是购物剁手狂欢节&#xff0c;也是货比三家纠结得不行的选择困难症复发期。而现在&#xff0c;Pura 70 能够帮助我们解决不够了解商品、选择困难症等问题啦。 小艺圈选&#xff0c;圈出你感兴趣的商品&#xff0c;快速货比三家 利用指关…

175页PPTBCG某企业健康智能制造与供应链战略规划建议书

智能制造与供应链战略规划方法论是一个系统性、科学性的框架&#xff0c;旨在指导企业实现智能制造转型和供应链优化。以下是对这一方法论的核心内容的归纳和阐述&#xff1a; 一、智能制造的目标与原则 明确智能制造目标&#xff1a; 提高生产效率&#xff1a;通过引入自动…

【VS+QT】联合开发踩坑记录

最新更新日期&#xff1a;2024/11/05 0. 写在前面 因为目前在做自动化产线集成软件开发相关的工作&#xff0c;需要用到QT&#xff0c;所以选择了VS联合开发&#xff0c;方便调试。学习QT的过程中也踩了很多坑&#xff0c;在此记录一下&#xff0c;提供给各位参考。 1. 环境配…

flutter 专题四 Flutter渲染流程

一、 Widget - Element - RenderObject关系 二、 Widget 、Element 、RenderObject 分别表示什么 2.1 Widget Widget描述和配置子树的样子 Widget就是一个个描述文件&#xff0c;这些描述文件在我们进行状态改变时会不断的build。但是对于渲染对象来说&#xff0c;只会使用最…

芯片需要按一下keyup或者复位按键虚拟或者下载之后芯片能下载却运行不了或者需要额外供电。

这些问题很有可能是因为外围电路器件幅值与设计不同的存在&#xff0c;导致你需要外部供电才能实现一个正常运行&#xff0c;可以检查一下外围电路在供电区域的电流区&#xff0c;电阻幅值是否和原理图设计时看的一模一样或者直接更换 因为按键会失灵&#xff0c;首先检查复位按…