设计模式21-组合模式

news2025/1/11 13:56:46

设计模式21-组合模式(Composite Pattern)

  • 写在前面
  • 动机
  • 定义与结构
    • 定义
    • 结构
      • 主要类及其关系
  • C++代码推导
  • 优缺点
  • 应用场景
  • 总结
  • 补充
    • 叶子节点不重载这三个方法
    • 叶子节点重载这三个方法
    • 结论

写在前面

数据结构模式
常常有一些组件在内部具有特定的数据结构。如何让客户程序依赖这些特定的数据结构,将极大的破坏组件的复用。那么这个时候将这些特定数据结构封装在内部。在外部提供统一的接口来实现与特定数据结构无关的访问。是一种行之有效的解决方案。

典型模式

  • 组合模式
  • 迭代器模式

动机

  • 软件在某些情况下,客户代码过多的依赖于对象容器复杂的内部实现结构,对象容器内部实现结构而非抽象接口的变化将引起客户代码的频繁变化。代码的维护性,扩展性等弊端。
  • 那么如何将客户代码与复杂的对象容器结构进行解耦?让对象容器自己来实现自身的复杂结构。而使得客户代码就像处理简单对象一样来实现处理复杂的对象容器?
  • 在软件开发中,有时我们需要处理树形结构的数据。例如,图形编辑器中一个复杂图形可以由多个简单图形(如线条、圆形、矩形等)组合而成。无论是单个简单图形还是复杂图形的组合,从操作上看,它们应当被视为一个整体。组合模式的动机是通过将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以一致地处理单个对象和组合对象。

定义与结构

定义

组合模式允许你将对象组合成树形结构来表示“部分-整体”的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性。

结构

在这里插入图片描述

这张图是一个UML(统一建模语言)类图,用于展示软件系统中类之间的结构和关系。通过图形化的方式描述了类的属性、操作(方法)以及类之间的继承、关联等关系。

主要类及其关系

  1. Client(客户端)

    • 继承自Component类。
    • 表示一个使用组件的客户端实体。客户端通过继承Component类,获得了对子节点的操作能力,包括添加、删除和获取子节点。
  2. Component(组件)

    • 是一个抽象类,代表了一个具有子组件概念的通用组件。
    • 它定义了三个操作(方法):
      • Add(Component): 添加一个子组件。
      • Remove(Component): 移除一个子组件。
      • GetChild(int): 根据索引获取子组件。
    • 还有一个属性children,用于存储子组件的集合,尽管这个属性在图中没有明确标出,但根据UML的惯例和类的操作可以推断出来。
  3. Leaf(叶子节点)

    • 继承自Component类。
    • 表示没有子节点的组件,即树的叶子。
    • 它同样定义了操作列表,但这里特别指出了一个forall g in children的操作,这实际上是一个伪代码或注释,因为叶子节点没有子节点(children为空或不存在),所以这个操作在叶子节点上下文中不适用。这里的展示可能只是为了强调Leaf类继承自Component类,并保留了Component的接口结构。
  4. Composite(复合节点)

    • 继承自Component类。
    • 表示具有多个子组件的复合结构,如树中的非叶子节点。
    • 它除了具有Component类定义的操作外,还特别指出了对子节点g的操作(g.Operation():),这里g代表了一个子组件的实例,这个注释或伪代码表明Composite类可以对其子节点执行某种操作,但没有具体说明是什么操作,这取决于实际的应用场景。

这张UML类图展示了一个典型的组合模式(Composite Pattern)的结构,其中Component是一个抽象类,代表了一个具有共同接口的对象,这个接口允许在组件的单个对象和组合对象之间进行一致的操作。Client类展示了如何使用这个结构,而LeafComposite类则分别代表了结构中的叶子节点和复合节点。通过这种方式,系统可以以统一的方式处理单个对象和组合对象,简化了客户端代码并提高了系统的可扩展性。

C++代码推导

以下是一个使用组合模式的C++代码示例,模拟一个文件系统,其中目录可以包含文件或其他子目录。

抽象组件类:

#include <iostream>
#include <vector>
#include <string>

// 抽象组件类,表示文件系统的节点
class FileSystemComponent {
public:
    virtual void showDetails(int indent = 0) const = 0;
    virtual void add(FileSystemComponent* component) {
        throw std::runtime_error("Cannot add to a leaf component");
    }
    virtual void remove(FileSystemComponent* component) {
        throw std::runtime_error("Cannot remove from a leaf component");
    }
    virtual ~FileSystemComponent() = default;
};

叶子节点类(文件):

class File : public FileSystemComponent {
private:
    std::string name;

public:
    File(const std::string& name) : name(name) {}

    void showDetails(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name << std::endl;
    }
};

组合节点类(目录):

class Directory : public FileSystemComponent {
private:
    std::string name;
    std::vector<FileSystemComponent*> components;

public:
    Directory(const std::string& name) : name(name) {}

    void add(FileSystemComponent* component) override {
        components.push_back(component);
    }

    void remove(FileSystemComponent* component) override {
        components.erase(std::remove(components.begin(), components.end(), component), components.end());
    }

    void showDetails(int indent = 0) const override {
        std::cout << std::string(indent, ' ') << name << "/" << std::endl;
        for (const auto& component : components) {
            component->showDetails(indent + 2);
        }
    }

    ~Directory() {
        for (auto component : components) {
            delete component;
        }
    }
};

客户端代码:

int main() {
    FileSystemComponent* rootDir = new Directory("root");
    FileSystemComponent* homeDir = new Directory("home");
    FileSystemComponent* userDir = new Directory("user");

    FileSystemComponent* file1 = new File("file1.txt");
    FileSystemComponent* file2 = new File("file2.txt");
    FileSystemComponent* file3 = new File("file3.txt");

    rootDir->add(homeDir);
    homeDir->add(userDir);
    userDir->add(file1);
    userDir->add(file2);
    homeDir->add(file3);

    rootDir->showDetails();

    delete rootDir;

    return 0;
}

运行结果:

root/
  home/
    user/
      file1.txt
      file2.txt
    file3.txt

优缺点

优点:

  1. 统一性:组合模式使得客户端可以一致地处理单个对象和组合对象,统一了对叶子节点和组合节点的操作。
  2. 灵活性:可以很方便地增加新的节点类型(如新的文件类型或目录类型),符合开闭原则。
  3. 简化客户端代码:客户端无需关心处理的是单个对象还是组合对象,减少了代码复杂性。

缺点:

  1. 复杂性:可能会导致系统中类的数量增加,特别是当需要支持复杂的树形结构时。
  2. 难以限制组合:在组合模式中,很难限制哪些组件可以组合在一起,容易导致不合理的组合结构。

应用场景

组合模式在以下场景中应用较多:

  1. 需要表示树形结构的场景:如文件系统、组织结构、UI组件树等。
  2. 需要统一处理单个对象和组合对象的场景:如图形编辑器中的简单图形和组合图形。
  3. 需要动态构建部分-整体结构的场景:如菜单和子菜单的构建,产品配置和子组件的构建。

总结

  • 组合模式通过将对象组合成树形结构来表示“部分-整体”的层次结构,使得客户端可以一致地处理单个对象和组合对象。它在需要处理树形结构的数据时非常有效,能够简化客户端代码,并提供很好的扩展性。然而,由于可能引入更多的类,特别是当系统的组合结构复杂时,需要注意管理组合的复杂性。
  • 组合模式采用树形结构来实现普遍存在的对象容器,从而将一对多的关系转化为一对一的关系。使得客户代码可以一致的复用处理对象和和对象容器。无需关心处女的是单个对象还是组合的对象容器
  • 客户代码与复杂的对象容器结构解耦是组合模式的核心思想。解耦之后,客户代码将与纯粹的抽象接口而非对象容器的内部时间结构发生依赖,从而更能应对变化。
  • 组合模式在具体的实现中可以让父对象中的子对象反向追溯。如果富对线有频繁的便利需求,可以使用缓存技巧来改善效率。

补充

在组合模式中,叶子节点通常不需要实现(即重载)Add(Component), Remove(Component), GetChild(int)这三个方法,因为叶子节点不包含子节点。这些方法主要用于组合节点(Composite)以便管理子节点。但有时,为了简化代码或提高灵活性,叶子节点也可能会实现这些方法。以下是叶子节点重载与不重载这三个方法的优缺点对比。

叶子节点不重载这三个方法

实现方式:

在叶子节点中,这些方法通常被声明但不实现(在C++中通常可以抛出异常或者是空实现)。叶子节点不需要管理子组件。

class Leaf : public Component {
public:
    void Add(Component* component) override {
        throw std::runtime_error("Leaf nodes do not support Add operation");
    }

    void Remove(Component* component) override {
        throw std::runtime_error("Leaf nodes do not support Remove operation");
    }

    Component* GetChild(int index) override {
        throw std::runtime_error("Leaf nodes do not support GetChild operation");
    }

    void Operation() override {
        // 具体叶子节点的操作实现
    }
};

优点:

  1. 清晰的语义:叶子节点明确不支持子节点管理操作,这使得代码的意图更加清晰,避免了误用。
  2. 更强的类型安全:由于明确抛出异常或不实现,可以在运行时捕捉到错误,而不是让无意义的操作通过。
  3. 符合职责分离原则:叶子节点只专注于具体操作,不需要处理与子节点相关的逻辑。

缺点:

  1. 客户端代码需要做额外的检查:客户端需要知道一个组件是否是叶子节点,以避免调用不支持的方法,可能增加了客户端的复杂性。
  2. 减少了一致性:对客户端来说,调用这些方法会抛出异常或导致错误,这可能会影响代码的一致性和简洁性。

叶子节点重载这三个方法

实现方式:

叶子节点实现(重载)这些方法,但不执行任何操作或返回特定值,如nullptr

class Leaf : public Component {
public:
    void Add(Component* component) override {
        // 叶子节点不支持添加操作,但实现了这个方法
    }

    void Remove(Component* component) override {
        // 叶子节点不支持移除操作,但实现了这个方法
    }

    Component* GetChild(int index) override {
        return nullptr; // 叶子节点没有子节点,返回空指针
    }

    void Operation() override {
        // 具体叶子节点的操作实现
    }
};

优点:

  1. 简化客户端代码:客户端代码不需要检查节点类型,可以统一调用Add, Remove, GetChild,简化了代码逻辑。
  2. 提高一致性:所有组件(叶子节点和组合节点)都实现了相同的接口,提供了一致的编程接口。
  3. 增加灵活性:在未来扩展时,如果叶子节点需要支持子节点管理,可以直接扩展已有方法。

缺点:

  1. 隐藏潜在错误:叶子节点实现了不应该执行的操作(如AddRemove),可能导致误用而不易发现。
  2. 不符合职责分离原则:叶子节点本不应该涉及子节点管理操作,实现这些方法可能违反单一职责原则。
  3. 占用资源:虽然通常影响很小,但实现这些无操作的方法也会占用一些资源(例如代码空间),特别是在资源受限的环境中。

结论

  1. 不重载方法的情况:适用于严格遵循职责分离原则的场景。通过不重载方法,明确区分了叶子节点和组合节点的职责,使得代码更清晰,类型安全性更高。这种方式适合对系统稳定性和安全性要求较高的场合,或在需要明确捕获误用场景的应用中使用。

  2. 重载方法的情况:适用于追求客户端代码简单性和一致性的场景。通过重载这些方法,客户端不需要区分叶子节点和组合节点,统一处理所有组件,减少了代码的复杂性。这种方式适合在系统中灵活性要求较高、且不易出错的场合。

综上,选择是否重载这些方法取决于具体应用的需求、开发团队的编码习惯和系统的复杂性。如果系统需要严格的职责区分和类型安全性,建议不重载这些方法;如果系统追求统一性和简洁性,可以考虑重载这些方法。

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

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

相关文章

StreamUtils 流处理工具

一、工具类展示 提供对集合的过滤、拼接、排序、MAP转化、分组、转为SET集合等方法 /*** stream 流工具类**/ NoArgsConstructor(access AccessLevel.PRIVATE) public class StreamUtils {/*** 将collection过滤** param collection 需要转化的集合* param function 过滤方法…

【Linux Install】Ubuntu20, Windows10 双系统安装

1. 制作启动盘 1.1 下载 Ubuntu 系统镜像 ISO 文件 从 Ubuntu 官网下载 (https://cn.ubuntu.com/download/desktop)。官网访问慢的&#xff0c;从国内镜像点下。 1.2 烧录 Ubuntu ISO 镜像 下载 Rufus&#xff1a;从Rufus官网下载 Rufus 工具。 插入U 盘&#xff1a;将U盘插…

mysql-增添轮播图

使用工具Navicat连接mysql: 首先 然后 需要注意的是需要上面两个步骤执行之后,再点击连接测试才可以成功,其他单独连接测试都不成功,然后点击确定即可!!!!! MySQL修改: 首先,进入mysql mysql -u root -p 密码忘记参考教程:Linux错误 ERROR 1045 (28000): Acce…

数据结构入门——07堆

1.堆 堆&#xff08;Heap&#xff09;是一种特殊的完全二叉树数据结构&#xff0c;具有以下两个主要特性&#xff1a; 结构特性&#xff1a; 堆是一棵完全二叉树&#xff0c;即除了最后一层的叶子节点外&#xff0c;每一层都是满的&#xff0c;最后一层的叶子节点从左向右依次…

西安国际数字影像产业园作为一个数字创意孵化园的实际情况怎么样?

在科技飞速发展的今天&#xff0c;数字创意产业正迅速崛起&#xff0c;成为全球经济的新增长点。西安国际数字影像产业园作为中国西部数字创意产业的领军者&#xff0c;凭借其独特的优势和发展策略&#xff0c;迅速崭露头角&#xff0c;成为全国瞩目的焦点。那么&#xff0c;是…

前端统计SDK设计和实现

前端统计的范围 访问量 PV自定义事件性能&#xff0c;错误 前端统计的实现 发送统计数据 不用 axios ( 因为统计服务器通常由第三方提供&#xff0c;需要跨域 )&#xff0c;而用 img 发送&#xff0c;因为可跨域&#xff0c;且兼容性非常好 自定义事件的统计 pv 的统计 性能…

【鸿蒙学习】HarmonyOS应用开发者基础 - 构建更加丰富的页面之Tabs(三)

学完时间&#xff1a;2024年8月14日 一、前言叨叨 学习HarmonyOS的第六课&#xff0c;人数又成功的降了500名左右&#xff0c;到了3575人了。 本文接上一文章【鸿蒙学习】HarmonyOS应用开发者基础 - 构建更加丰富的页面&#xff08;一&#xff09;&#xff0c;继续记录构建更…

探索数据结构:AVL树的分析与实现

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;数据结构与算法 贝蒂的主页&#xff1a;Betty’s blog 1. AVL树的介绍 在前面我们学习二叉搜索树时知道&#xff0c;在数据有序…

鸿蒙开发APP应用UX体验标准

基础体验 应用导航 3.1.1.1 系统返回 页面布局 3.1.2.1 布局基础要求 3.1.2.2 挖孔区适配 人机交互 3.1.3.1 避免与系统手势冲突3.1.3.2 典型手势时长设计3.1.3.3 点击热区 视觉风格 3.1.4.1 色彩对比度3.1.4.2 字体大小 3.1.4.3 图标 3.1.4.3.1 应用图标3.1.4.3.2 界…

统一响应结果封装,Result类的实现【后端 06】

统一响应结果封装&#xff0c;Result类的实现 在开发Web应用或API接口时&#xff0c;如何优雅地处理并返回响应结果是每个开发者都需要考虑的问题。统一响应结果封装&#xff08;Unified Response Encapsulation&#xff09;作为一种广泛采用的实践&#xff0c;不仅提高了API的…

快讯 | OpenAI 找回场子:chatgpt-4o-latest 刷新多项AI跑分纪录

在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展&#xff0c;捕捉行业动态&#xff1b;提供深入的新闻解读&#xff0c;助您洞悉技术背后的逻辑&#xff1b;汇聚行业专家的见解&#xff0c;…

LeetCode 205 同构字符串

题目 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。 如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。 每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字符上&#xff0c…

边缘智能:让每一个温室都成为计算中心

&#xff08; 于景鑫 国家农业信息化工程技术研究中心&#xff09;当人工智能的浪潮席卷全球&#xff0c;大语言模型&#xff08;LLM&#xff09;引领智能风潮之时&#xff0c;"智慧农业"也摩拳擦掌跃跃欲试。设施农业作为现代农业的翘楚&#xff0c;正站在数智化变革…

C语言典型例题38

《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 例题3.5 写程序&#xff0c;判断某一年是否为闰年 代码&#xff1a; //《C程序设计教程&#xff08;第四版&#xff09;——谭浩强》 //例题3.5 写程序&#xff0c;判断某一年是否为闰年//相关知识&#xff1a;如果…

观存储历史,论数据未来

数据存储 这几天我反复观看了腾讯云社区的《中国数据库前世今生》纪录片&#xff0c;每次的感受都大相径庭。以下是我在这段时间里对纪录片的两个不同感想&#xff0c;希望感兴趣的小伙伴们也能去观看一番。 一个是关于国产数据库的发展趋势的探讨&#xff1a;https://blog.c…

使用 C# 反射查询程序集的元数据 (LINQ)

文章目录 1. 反射概述2. LINQ 概述3. 使用反射和 LINQ 查询程序集的元数据4. 扩展&#xff1a;查询字段和属性5. 扩展示例&#xff1a;查询公共类及其属性和方法6. 总结 在 C# 中&#xff0c;反射是一个强大的工具&#xff0c;它允许我们在运行时检查程序集、类型、方法等的元数…

机器学习速成第二集——监督学习之回归(理论部分)!

目录 回归算法 线性回归与非线性回归在实际应用中的优缺点比较是什么&#xff1f; 线性回归的优缺点 非线性回归的优缺点 优点&#xff1a; 缺点&#xff1a; 多项式回归模型如何选择最佳的多项数以提高预测准确性&#xff1f; 岭回归和套索回归在防止过拟合方面的具体…

【屏驱MCU】实现文件路径的的挂载

说明&#xff1a;本文涉及到一些底层的 .py 编译脚本以及编辑原理&#xff0c;笔者也不是完全明白&#xff0c;本文的主要目的是介绍一下流程&#xff0c;供小白使用。 接上文&#xff1a;【屏驱MCU】RT-Thread 文件系统接口解析 屏驱MCU系列文章 【屏显MCU】多媒体接口总结&am…

【Python学习-UI界面】PyQt5 小部件6- QComboBox

样式如下: 一个 QComboBox 对象呈现一个下拉列表供选择。它在表单上占用的屏幕空间最小&#xff0c;仅显示当前选定项。 可以将组合框设置为可编辑&#xff1b;还可以存储像素映射对象。 常用方法如下&#xff1a; 序号方法描述1addItem将字符串添加到集合中2addItems在列…

第十一章、 Java常用类

第十一章、 Java常用类 11.1 包装类 11.1.1 包装类的分类 针对八种基本数据类型相应的引用类型-包装类有了类的特点&#xff0c;就可以调用类中的方法。 11.1.2 包装类和基本数据的转换 Jdk5前的手动装箱和拆箱方式&#xff0c;装箱&#xff1a;基本类型->包装类型&am…