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

news2024/12/23 7:02:56

文章目录

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

一、引言

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

组合模式主要用来处理树形结构的数据,例如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.txt

After 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.coloradmin.cn/o/2230404.html

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

相关文章

【C/C++】qsort函数的学习与使用

零.导言 在之前的文章中&#xff0c;我介绍了冒泡排序&#xff0c;即按ASCII码值把元素从小到大排序&#xff08;文章链接我放在了第五部分&#xff0c;有兴趣的小伙伴可以求看看&#xff09;。而今天我将继续介绍qsort函数&#xff0c;这个函数可以起到和冒泡排序一样的作用&a…

前段(vue)

目录 跨域是什么&#xff1f; SprinBoot跨域的三种解决方法 JavaScript 有 8 种数据类型&#xff0c; 金额的用什么类型。 前段 区别 JQuery使用$.ajax()实现异步请求 Vue 父子组件间的三种通信方式 Vue2 和 Vue3 存在多方面的区别。 跨域是什么&#xff1f; 跨域是指…

【elkb】索引生命周期管理

索引生命周期管理 Index lifecycle management(索引生命周期管理)是elasticsearch提供的一种用于自动管理索引的生命周期的功能。允许使用者定义索引的各个阶段&#xff0c;从创建至删除。并允许使用者在每个阶段定义索引需要执行的特定动作。这些动作包含索引创建&#xff0c…

基于SSM志愿者招募系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;志愿组织管理&#xff0c;组织信息管理&#xff0c;组织申请管理&#xff0c;志愿活动管理活动报名管理 用户账号功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;…

msys2更换国内源(多个文件(不是3个文件的版本!))

msys2更换国内源 起因排查答案如下mirrorlist.mingw64mirrorlist.ucrt64mirrorlist.mingw32mirrorlist.mingwmirrorlist.clang64mirrorlist.clang32mirrorlist.msys 不想看经过的直接跳到答案 起因 查了很多个教程大部分都是【打开MSYS2软件内的\etc\pacman.d\ 中3个文件&…

Spring Boot框架下的信息学科平台系统架构设计

3系统分析 3.1可行性分析 通过对本基于保密信息学科平台系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本基于保密信息学科平台系统采用Spring Boot框架&a…

基于Python可视化的热门微博数据分析系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于pythondjangovueMySQL的热…

ffplay 实现视频流中音频的延迟

ffplay -rtsp_transport tcp -i rtsp://admin:1234qwer192.168.1.64:554/Streaming/Channels/101 -vn -af "adelay5000|5000"在这个命令中&#xff1a; -vn 参数表示只播放音频。 -af "adelay5000|5000" 参数表示将音频延迟5000毫秒&#xff08;即5秒&…

Iceoryx2:高性能进程间通信框架(中间件)

文章目录 0. 引言1. 主要改进2. Iceoryx2 的架构3. C示例代码3.1 发布者示例&#xff08;publisher.cpp&#xff09;3.2 订阅者示例&#xff08;subscriber.cpp&#xff09; 4. 机制比较5. 架构比较6. Iceoryx vs Iceoryx2参考资料 0. 引言 Iceoryx2 是一个基于 Rust 实现的开…

HTML+javaScript+CSS

文章目录 HTMLjavaScriptCSS属性区块表单层叠样式表选择器常用属性盒子模型相关属性浮动float定位&#xff08;position&#xff09; JS操作节点事件点击事件onclick()聚焦事件、失焦事件鼠标移入移出事件 定时任务延迟定时任务重复定时任务 判断哪个单选框被选中设置按钮失效冒…

跟着红队笔记学习 tmux:渗透测试中的多终端利器

内容预览 ≧∀≦ゞ 跟着红队笔记学习 tmux&#xff1a;渗透测试中的多终端利器进入 tmux 前的准备tmux 概念简介tmux 基础操作会话管理命令会话管理快捷键会话内和会话外命令的区别 tmux 窗口和面板管理新建和管理窗口分割窗口为面板切换面板面板放大与恢复调整面板大小关闭面板…

服务器数据恢复—DELL EqualLogic PS6100系列存储简介及如何收集故障信息?

DELL EqualLogic PS6100系列存储采用虚拟ISCSI SAN阵列&#xff0c;支持VMware、Solaris、Linux、Mac、HP-UX、AIX操作系统&#xff0c;提供全套企业级数据保护和管理功能&#xff0c;具有可扩展性和容错功能。DELL EqualLogic PS6100系列存储介绍&#xff1a; 1、上层应用基础…

【力扣】Go语言回溯算法详细实现与方法论提炼

文章目录 一、引言二、回溯算法的核心概念三、组合问题1. LeetCode 77. 组合2. LeetCode 216. 组合总和III3. LeetCode 17. 电话号码的字母组合4. LeetCode 39. 组合总和5. LeetCode 40. 组合总和 II小结 四、分割问题6. LeetCode 131. 分割回文串7. LeetCode 93. 复原IP地址小…

【深度学习】实验 — 动手实现 GPT【三】:LLM架构、LayerNorm、GELU激活函数

【深度学习】实验 — 动手实现 GPT【三】&#xff1a;LLM架构、LayerNorm、GELU激活函数 模型定义编码一个大型语言模型&#xff08;LLM&#xff09;架构 使用层归一化对激活值进行归一化LayerNorm代码实现scale和shift 实现带有 GELU 激活的前馈网络测试 模型定义 编码一个大…

基于springboot+vue车辆充电桩管理系统

基于springbootvue车辆充电桩管理系统 摘 要 随着信息化时代的到来&#xff0c;管理系统都趋向于智能化、系统化&#xff0c;车辆充电桩管理系统也不例外&#xff0c;但目前国内仍都使用人工管理&#xff0c;市场规模越来越大&#xff0c;同时信息量也越来越庞大&#xff0c;…

WordPress网站添加嵌入B站视频,自适应屏幕大小,取消自动播放

结合bv号 改成以下嵌入式代码&#xff08;自适应屏幕大小,取消自动播放&#xff09; <iframe style"width: 100%; aspect-ratio: 16/9;" src"//player.bilibili.com/player.html?isOutsidetrue&bvidBV13CSVYREpr&p1&autoplay0" scrolling…

BLG与T1谁会赢?python制作预测程序,结果显示,BLG将打败T1

决赛预测 2024英雄联盟全球总决赛 2024年英雄联盟全球总决赛&#xff0c;今天晚上&#xff08;2024年11月2日22点&#xff09;就要开始了&#xff01;今年的总决赛的队伍是BLG与T1。当然一些老的lol玩家&#xff0c;现在可能对于lol关注不多&#xff0c;并不清楚这两个队伍。…

AI-基本概念-向量、矩阵、张量

1 需求 需求&#xff1a;Tensor、NumPy 区别 需求&#xff1a;向量、矩阵、张量 区别 2 接口 3 示例 4 参考资料 【PyTorch】PyTorch基础知识——张量_pytorch张量-CSDN博客

【笔面试常见题:三门问题】用条件概率、全概率和贝叶斯推导

1. 问题介绍 三门问题&#xff0c;又叫蒙提霍尔问题&#xff08;Monty Hall problem&#xff09;&#xff0c;以下是蒙提霍尔问题的一个著名的叙述&#xff0c;来自Craig F. Whitaker于1990年寄给《展示杂志》&#xff08;Parade Magazine&#xff09;玛丽莲沃斯莎凡特&#x…

Core日志 Nlog

资料 资料 资料 直接在NuGet里面搜索NLog.Web.AspNetCore&#xff0c;然后进行安装即可&#xff0c;