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.xdnf.cn/news/2542.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

电子商城购物平台的设计与开发+ssm(lw+演示+源码+运行)

摘 要 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;电子商城购物平台小程序被用户普遍使用&#xff0c;为方便…

用离线的方式(使用U盘)将Qt文件装载到开发板

第一步&#xff1a;打开虚拟机软件&#xff0c;加载Linux系统进入桌面 桌面 第二步&#xff1a;将U盘插入电脑&#xff0c;挂载到虚拟机中选择连接到虚拟机&#xff0c;虚拟机名称为alientek U盘接入虚拟机 第三步&#xff1a;将mp157开发板一端连接在USB_TTL接口&#xff…

Android 字节飞书面经

Android 字节飞书面经 文章目录 Android 字节飞书面经一面二面 一面 1. 线程是进程的一部分&#xff0c;一个线程只能属于一个进程&#xff0c;而一个进程可以有多个线程&#xff0c;但至少有一个线程。 2. 根本区别&#xff1a;进程是操作系统资源分配的基本单位&#xff0c;…

针对告警数量、告警位置、告警类型等参数进行统计,并做可视化处理的智慧能源开源了。

一、简介 AI视频监控平台, 是一款功能强大且简单易用的实时算法视频监控系统。愿景在最底层打通各大芯片厂商相互间的壁垒&#xff0c;省去繁琐重复的适配流程&#xff0c;实现芯片、算法、应用的全流程组合&#xff0c;减少企业级应用约 95%的开发成本&#xff0c;在强大视频算…

Linux系统的入门使用

前言一、常用操作以及概念 快捷键求助关机PATHsudo包管理工具发行版VIM 三个模式GNU开源协议 二、磁盘 磁盘接口磁盘的文件名 三、分区 分区表开机检测程序 四、文件系统 分区与文件系统组成文件读取磁盘碎片blockinode目录日志挂载目录配置 五、文件 文件属性文件与目录的基本…

软考系统分析师知识点三二:案例知识点三

前言 今年报考了11月份的软考高级&#xff1a;系统分析师。 考试时间&#xff1a;11月9日。 倒计时&#xff1a;5天。 目标&#xff1a;优先应试&#xff0c;其次学习&#xff0c;再次实践。 复习计划第三阶段&#xff1a;总结案例知识点&#xff0c;并作为论文的框架知识…

无人机维护保养、部件修理更换技术详解

无人机作为一种精密的航空设备&#xff0c;其维护保养和部件修理更换是确保飞行安全、延长使用寿命的重要环节。以下是对无人机维护保养、部件修理更换技术的详细解析&#xff1a; 一、无人机维护保养技术 1. 基础构造理解&#xff1a; 熟悉无人机的基本构造&#xff0c;包括…

高校大数据实训平台介绍

高校大数据实验室架构 具体实训平台介绍 编程实训平台 1、大数据开发实训平台 大数据开发实训平台是面向实训课和课后训练的编程实训平台&#xff0c;平台底层基于Docker技术&#xff0c;采用容器云部署方案&#xff0c;预装大数据相关课程教学所需的实训环境…

SQL基础—2

1.左外连接查询&#xff08;left join on&#xff09; A - A∩B 左外连接查询两张表条件都满足的数据&#xff0c;以及左边表(A表)存在的数据(以左边表为主查询表)。 A - A∩B (A和A交B)。 示例&#xff1a;使用左外连接将dept表作为主查询表&#xff0c;查询员工编号、员工姓…

R语言贝叶斯:INLA下的贝叶斯回归、生存分析、随机游走、广义可加模型、极端数据的贝叶斯分析

原文链接&#xff1a;R语言贝叶斯&#xff1a;INLA下的贝叶斯回归、生存分析、随机游走、广义可加模型、极端数据的贝叶斯分析https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247625527&idx8&snba4e50376befd94022519152609ee8d0&chksmfa8daad0cdfa23c6…

如何自学机器学习?

自学机器学习可以按照以下步骤进行&#xff1a; 一、基础知识准备 数学基础&#xff1a; 高等数学&#xff1a;学习微积分&#xff08;包括导数、微分、积分等&#xff09;、极限、级数等基本概念。这些知识是后续学习算法和优化方法的基础。 线性代数&#xff1a;掌握矩阵…

wpf 制作丝滑Flyout浮出侧边栏Demo (Mahapps UI框架)

Flyout 属性 CloseButtonVisibility: 设置为 Collapsed&#xff0c;意味着关闭按钮不可见。TitleVisibility: 设置为 Collapsed&#xff0c;意味着标题不可见。IsPinned: 设置为 True&#xff0c;意味着这个 Flyout 会固定住&#xff0c;不会自动关闭。Opacity: 设置为 1&…

6个步骤,轻松搞定Linux上web UI自动化测试!

对于web端的UI自动化&#xff0c;相信大家都不会陌生&#xff0c;因为很多小伙伴都做过&#xff0c;或者了解到过&#xff0c;但是小编相信&#xff0c;大多数了解到的都是通过windows系统上进行运行web端的UI自动化&#xff0c;在linux上面很少运行UI自动化或者如何执行自动化…

[论文阅读]Label-Only Membership Inference Attacks

Label-Only Membership Inference Attacks Proceedings of the 38th International Conference on Machine Learning Label-Only Membership Inference Attacks 只使用硬标签就可以判断是否是成员的方法&#xff0c;但是是在机器学习模型上。 通过分析模型在扰动下的预测标…

万宇科技闪耀创新舞台 荣膺潜在独角兽企业殊荣

2024年10月24日&#xff0c;在“2024东北亚(沈阳)人才交流大会暨中国潜在独角兽企业发展大会”上&#xff0c;长城战略咨询重磅发布《GEI中国潜在独角兽企业研究报告2024》&#xff0c;揭示了中国潜在独角兽企业群体的最新发展态势。其中&#xff0c;安徽万宇机械设备科技有限公…

Java Iterator 实现杨辉三角

一、问题描述 杨辉三角定义如下&#xff1a; 1/ \1 1/ \ / \1 2 1/ \ / \ / \1 3 3 1/ \ / \ / \ / \1 4 6 4 1/ \ / \ / \ / \ / \ 1 5 10 10 5 1 把每一行看做一个list&#xff0c;试写一个 Iterator&#xff0c;不断输出下一行的 list&#xf…

解决注册Kaggle出现的“Captcha must be filled out”问题

首先&#xff0c;出现这个问题后&#xff0c;就搜索了一下别的博主的方法。 使用header editor 插件 首先&#xff0c;下载扩建&#xff1a; 然后进行重定向&#xff1a; 管理之后&#xff0c;输入下面的地址&#xff0c;然后下载-保存&#xff1a; 但是&#xff0c;这条显然…

【Python】 select模块详解 所有程序猿必看!!!

要理解select.select模块其实主要就是要理解它的参数, 以及其三个返回值。 select()方法接收并监控3个通信列表&#xff0c; 第一个是所有的输入的data,就是指外部发过来的数据&#xff0c;第2个是监控和接收所有要发出去的data(outgoing data),第3个监控错误信息 在网上一直在…

JavaIO流操作

目录 简介 字节输入流 获取字节输入流 读 关闭输入流 字节输出流 获取字节输出流 写 换行符 刷新 关闭输出流 字符流输入流 获取字符输入流 读 关闭输入流 字符输出流 获取字符输出流 写 换行符 刷新 关闭输出流 简介 IO流分为两大派系&#xff1a; …

大数据之Hadoop集群

Hadoop集群介绍&#xff1f;Hadoop集群的优缺点及应用场景&#xff1f;Hadoop集群搭建&#xff1f;Hadoop架构&#xff1f; Hadoop集群介绍 Hadoop集群是由多台计算机&#xff08;节点&#xff09;组成的一个分布式计算系统&#xff0c;主要用于处理大规模的数据集。以下是对Ha…