完整教程:设计模式(C++)详解——组合模式(Composite Pattern)(2)
各位亲爱的程序员朋友们!今天,我们将开启一场关于设计模式的华丽冒险,而我们的主角,就是那个能让代码像搭积木一样构建复杂世界的组合模式(Composite Pattern)!
准备好了吗?我们将像解剖一只精美的瑞士手表一样,把组合模式从内到外、从理论到实践、从起源到未来,掰开了、揉碎了,讲得明明白白、生动有趣。我们的目标是:不仅要让你看得懂,更要让你看得爽,最后能用得上,甚至爱不释手!
<摘要>
组合模式,一个名字听起来有点“组装”意味的设计模式,实则是处理树形结构数据的“神兵利器”。它的核心魔法在于:用一致的方式对待单个对象和对象组合,从而让客户端代码从繁琐的类型判断中解放出来,轻松驾驭任何复杂的层次结构。本文将化身一场趣味横生的技术探索之旅,带你穿越组合模式的设计哲学、两种实现派系(透明vs安全)的“江湖恩怨”,并通过文件系统、UI组件、组织架构等活生生的C++案例,让你亲手实践这“化繁为简”的编程艺术。我们将提供上万行带精辟注释的代码、精美的Mermaid图表、即拿即用的Makefile,让你在欢声笑语中彻底征服组合模式,成为一名真正的“结构大师”。
<解析>
1. 背景与核心概念:为什么我们需要“组合”?
1.1 一场来自“树形结构”的挑战
想象一下,你正在开发一款超酷的文件管理器。你的面前有两个截然不同的东西:文件(File) 和 目录(Directory)。
- 文件:是个“老实人”,自己有多大就是多大,肚子里没别的东西。
- 目录:是个“包租公”,自己本身没大小,但肚子里可以装一堆“房客”(文件和子目录)。
现在,产品经理给你提了个需求:计算某个目录的总大小。
如果你的代码是这样的:
// 伪代码:噩梦的开始
if (对象是文件) {
return 文件.size;
} else if (对象是目录) {
int 总大小 = 0;
for (目录里的每一个孩子) {
if (孩子是文件) {
总大小 += 孩子.size;
} else if (孩子是目录) {
// 哦豁!递归来了!
总大小 += 计算目录大小(孩子);
// 又要开始if-else判断...
}
}
return 总大小;
}
Stop! 这也太丑了! 这种代码充斥着“臭味”:
- 重复的判断逻辑:每进入一层,都要做相同的类型判断。
- 僵化不灵活:如果以后要加入一种新的“符号链接”类型,你得把所有判断的地方改个遍,这违反了开闭原则(对扩展开放,对修改关闭)。
- 客户端代码过于复杂:客户端(调用方)必须了解整个层次结构的所有细节,耦合度极高。
我们渴望一种更优雅的方式。我们希望能像下面这样:
// 伪代码:梦想中的样子
int 计算大小(某个节点) {
return 某个节点->
getSize();
// 管它是文件还是目录,我只调一个方法!
}
是的!组合模式就是为了实现这个梦想而生的! 它告诉我们:“别管它是文件还是目录,它们都是‘文件系统节点’,都有一个getSize()方法。你就统一调这个方法,剩下的让它们自己内部折腾去!”
1.2 发展历程:从“四人帮”到现代编程
组合模式并不是什么新潮的概念。它最早在1994年,由Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides(这四位大佬被尊称为 Gang of Four, GoF)在他们的开山名著《设计模式:可复用面向对象软件的基础》中正式提出。
这本书如同设计模式的“圣经”,为无数挣扎于软件复杂性的程序员指明了道路。组合模式作为23种经典设计模式中的一种,属于结构型模式,专门负责如何将类和对象组合成更大的结构。
二十多年过去了,组合模式的思想不仅没有过时,反而在无数框架和系统中熠熠生辉:
- 图形界面(GUI)开发:Qt, MFC, Java AWT/Swing, .NET WinForms,几乎所有的UI框架都在使用组合模式来构建窗口、面板、按钮的层次树。
- 文档对象模型(DOM):网页中的HTML结构就是一个巨大的组合模式树,
<div>可以包含<p>和<span>,它们都是Node。 - 文件系统:正如我们的例子,是组合模式的绝佳体现。
- 组织结构:公司-部门-团队-员工,也是一种经典的组合关系。
- 现代游戏开发:游戏场景(Scene)由各种游戏对象(GameObject)组成,而游戏对象又可以包含子对象,形成一棵场景树。
1.3 核心概念:一张图看懂组合模式
组合模式的核心是构建一棵树,树上的每个节点都遵守同一个“约定”。让我们用一张UML类图来揭开它的神秘面纱:
classDiagram
direction TB
note for Component "声明所有组合对象的通用接口\n(透明方式)"
class Component {
>
+operation()
+add(Component)
+remove(Component)
+getChild(int) Component
// ... 其他公共方法
}
class Leaf {
+operation() // 实现自身的行为
// add, remove, getChild 通常抛出异常或空实现
}
class Composite {
-children: List~Component~
+operation() // 通常遍历children,委托调用
+add(Component) // 管理子组件
+remove(Component)
+getChild(int) Component
}
Component <|-- Leaf
Component <|-- Composite
Composite o-- "*" Component : children
这张图里的角色,我们一个一个来认识:
组件(Component) - 团队的“宪法”
- 它是谁? 一个抽象类或接口,是所有人的“老大”。
- 它做什么? 它定义了整个组合体系中所有对象的“基本公约”。比如,规定每个对象都必须有一个
getSize()方法。它同时还声明了用于管理子对象的方法(如add,remove),这让组合模式有了“透明”和“安全”之分,后面会细说。 - 口头禅: “我不管你们具体是谁,但在我这,都得遵守我的规矩!”
叶子(Leaf) - 团队的“一线员工”
- 它是谁? 继承自
Component,是树形结构中的基础单元,没有下属。 - 它做什么? 它实现了
Component定义的“基本公约”中属于它自己的那部分行为。比如,File实现了getSize(),返回自己的大小。对于那些它不该有的行为(比如管理下属),它通常选择抛出一个“臣妾做不到啊”的异常,或者直接忽略。 - 口头禅: “活是我干的,锅是我背的。别给我派小弟,我没有!”
- 它是谁? 继承自
复合体(Composite) - 团队的“项目经理”
- 它是谁? 也继承自
Component。它本身也是一个Component,但同时它还有一个“小本本”(比如一个列表),里面记录着它的所有子组件(这些子组件可以是Leaf,也可以是另一个Composite)。 - 它做什么? 它实现了
Component接口的行为。但它的工作方式通常是“甩手掌柜”:收到请求后,自己先做一些预处理,然后把工作委托(Delegate) 给自己的每一个子组件,最后再把子组件的结果汇总起来。它当然也实现了管理子组件的方法。 - 口头禅: “兄弟们,需求来了!A你做这个,B你做那个,做完汇总给我!”
- 它是谁? 也继承自
客户端(Client) - 团队的“大老板”
- 它是谁? 使用组合结构的代码。
- 它做什么? 它只和顶层的
Component抽象打交道。它不需要知道和自己对话的到底是一个“一线员工”(Leaf)还是一个“项目经理”(Composite)。它只管下达命令:“我不管你们内部怎么搞,我就要这个结果!” - 口头禅: “我只要结果!”
妙在哪里? 妙就妙在“大老板”(Client)根本不需要关心公司的层级有多深、有多少个“项目经理”。他只需要对最大的那个“总经理”(最顶层的Composite)发号施令,命令就会沿着层级结构一层一层地传递下去,直到每一个“一线员工”为止。这使得客户端代码极其简洁和稳定。
2. 设计意图与考量:透明还是安全?这是个问题
GoF在提出组合模式时,指出了一个关键的设计抉择点:如何设计Component接口中的子组件管理方法(如add, remove)? 这个抉择衍生出了两种流派:透明模式和安全模式。
2.1 透明模式(Transparent Composite)
核心思想: 在Component抽象类中直接声明所有管理子组件的方法(如add, remove, getChild)。
优点:
- 极致透明: 对客户端来说,所有的组件对象,无论它是
Leaf还是Composite,接口都是完全一致的。客户端可以毫无区别地对待它们,代码非常统一和优雅。 - 符合LSP: 从接口层面看,
Leaf完全能替代Composite,符合里氏替换原则(LSP)。
- 极致透明: 对客户端来说,所有的组件对象,无论它是
缺点:
- 不安全: 这是最大的代价。客户端可能会不小心对一个
Leaf调用add方法。这显然在逻辑上是说不通的,所以我们必须在Leaf类的add方法中抛出运行时异常(Runtime Exception)来进行约束。也就是说,错误只能在运行时被发现,无法在编译期拦截。
- 不安全: 这是最大的代价。客户端可能会不小心对一个
它像什么? 像一把“双刃剑”,锋利(好用)但容易伤到自己(不安全)。
2.2 安全模式(Safe Composite)
核心思想: 只在Composite类中声明管理子组件的方法。Component抽象类中不包含这些方法。
优点:
- 安全: 非常安全。因为
Leaf类根本没有add、remove这些方法,如果你试图对一個File调用add,编译器就会直接报错:“Error: no member named ‘add’ in ‘File’”。问题在编译阶段就被消灭了。
- 安全: 非常安全。因为
缺点:
- 失去透明性: 这是最大的代价。客户端现在必须知道它面对的对象到底是
Leaf还是Composite。因为如果你想给一个组件添加子节点,你必须先检查它是不是一个Composite对象。这又回到了之前需要类型判断的老路上,破坏了组合模式追求的“一致性”。
- 失去透明性: 这是最大的代价。客户端现在必须知道它面对的对象到底是
它像什么? 像一把“瑞士军刀”,安全可靠,但你需要知道哪个工具在哪,用法不统一。
2.3 抉择时刻:我该站哪边?
| 特性维度 | 透明模式 | 安全模式 |
|---|---|---|
| 设计位置 | 管理方法在Component中 | 管理方法只在Composite中 |
| 客户端代码 | 统一一致,无需类型判断 | 不统一,需判断类型才能调用管理方法 |
| 安全性 | 运行时安全(靠异常) | 编译期安全 |
| 是否符合LSP | 是 | 否(接口不同,无法完全替换) |
| 适用场景 | 客户端需要统一对待所有对象,且极少或从不会对叶子调用管理方法 | 客户端需要频繁管理子组件,且希望避免运行时错误 |
给你的建议:
- 大多数情况下,更推荐使用透明模式。 因为组合模式的精髓就在于“透明性”和“一致性”。虽然它理论上不安全,但在实践中,客户端通常很清楚自己在操作什么。你不会闲得无聊去试图把一个文件拖到另一个文件里面,对吧?这种错误相对罕见。用一点潜在的不安全,换来客户端代码极大的简洁和优雅,这笔买卖通常是划算的。
- 只有在那些子组件管理操作非常频繁,且逻辑复杂的场景下,才考虑安全模式。 比如,一个专门用于动态构建和修改树形结构的编辑器工具。
在我们的C++实现中,我们将选择透明模式,因为它更能体现组合模式的设计美学。
3. 实例与应用场景:看模式如何大显神通
理论说得再多,不如代码来得实在。下面我们通过三个经典的例子,让你看看组合模式是如何在各种场景下“呼风唤雨”的。
3.1 案例一:文件系统模拟(经典中的经典)
我们将用C++完整实现这个例子,代码在第四部分。这里先讲设计思路。
Component->FileSystemNode: 定义getSize(),getName(),addNode(),removeNode(),print()等方法。Leaf->File: 实现getSize(),返回文件字节数。addNode()等抛出std::runtime_error。Composite->Directory: 内部有一个std::vector<std::shared_ptr<FileSystemNode>>。getSize()会遍历所有子节点,递归求和。addNode()等操作这个集合。- 客户端: 构建树结构后,可以统一地对任何节点调用
getSize()或print(),无需关心它是文件还是目录。
神奇之处:Directory的getSize()方法里,只是一句简单的child->getSize()。它根本不用关心child是另一个Directory还是一个File。多态机制会自动帮我们找到正确的实现。这种递归和委托的机制,是组合模式优雅背后的核心动力。
3.2 案例二:图形用户界面(GUI)库
几乎所有GUI框架都是组合模式的巨大受益者。
Component->Widget/View: 定义draw(),getWidth(),addWidget(),setPosition()等方法。Leaf->Button,TextBox,Label: 实现draw(),负责在屏幕上绘制自己。Composite->Window,Panel,GroupBox: 内部包含子组件列表。其draw()方法会先绘制自己的背景边框,然后遍历所有子组件,调用它们的draw()方法。- 客户端: 要显示整个窗口,只需调用顶层
Window的draw()方法。整个UI树就会按顺序被绘制出来。
应用场景: 任何桌面应用、移动应用、Web前端框架(如React的虚拟DOM思想与组合模式高度契合)。
3.3 案例三:公司组织架构与薪酬计算
Component->OrganizationUnit: 定义getName(),getSalary()等方法。Leaf->Employee: 实现getSalary(),返回自己的工资。Composite->Department: 实现getSalary(),遍历所有子单元(可能是子部门或员工),递归求和。- 客户端: 要计算整个公司的总人力成本,只需调用
CEO办公室->getSalary()。
应用场景: ERP系统、人力资源管理软件、成本核算系统。
3.4 案例四:统一的事件处理机制
Component->EventHandler: 定义handleEvent(Event e)方法。Leaf->ClickListener,KeyListener: 实现handleEvent,处理特定的点击或键盘事件。Composite->EventComposite: 内部管理一系列事件处理器。当收到事件时,它可以将事件广播给所有注册的子处理器,或者按责任链模式依次传递。- 应用场景: 游戏引擎中的输入系统、Web服务器中的请求处理管道。
看到这里,你是不是已经跃跃欲试,想亲手实现一个了?别急,最精彩的部分来了!
4. 代码实现:手把手教你用C++打造一个文件系统
我们将采用透明模式,使用现代C++(C++17)的特性,编写一个健壮、易懂的文件系统模拟程序。代码将包含详尽的注释,甚至包括一些最佳实践和陷阱提醒。
4.1 项目结构
composite_demo/
├── include/
│ ├── FileSystemNode.h
│ ├── File.h
│ └── Directory.h
├── src/
│ ├── FileSystemNode.cpp
│ ├── File.cpp
│ ├── Directory.cpp
│ └── main.cpp
└── Makefile
4.2 头文件详解 (The Blueprints)
include/FileSystemNode.h (我们的“宪法”)
/**
* @file FileSystemNode.h
* @brief 文件系统节点抽象基类 (Component)
*
* 定义了组合模式中的抽象组件(Component)接口。
* 采用“透明式”设计,所有子类(文件和目录)共享同一套接口。
* 这使得客户端代码可以以统一的方式处理任何文件系统节点。
*/
#ifndef COMPOSITE_PATTERN_FILE_SYSTEM_NODE_H
#define COMPOSITE_PATTERN_FILE_SYSTEM_NODE_H
#include <string>
#include <memory>
#include <stdexcept>
// 用于抛出标准异常
// 前向声明,解决循环依赖
class FileSystemNode
;
// 使用智能指针别名,方便后续使用
using FileSystemNodePtr = std::shared_ptr<FileSystemNode>
;
class FileSystemNode
{
public:
/**
* @brief 构造函数
* @param name 节点名称
*/
explicit FileSystemNode(std::string name);
/**
* @brief 虚析构函数 (Virtual Destructor)
*
* 关键!确保通过基类指针删除派生类对象时,派生类的析构函数能被正确调用。
* 这是多态性的基本要求之一。
*/
virtual ~FileSystemNode() = default;
// C++11: 使用=default生成默认实现
// 禁止拷贝和赋值,因为通常树形结构节点所有权是唯一的
// C++11: 使用=delete显式删除函数
FileSystemNode(const FileSystemNode&
) = delete;
FileSystemNode&
operator=(const FileSystemNode&
) = delete;
/**
* @brief 获取节点名称
* @return 节点名称的常量引用,避免不必要的拷贝
*/
const std::string&
getName() const;
/**
* @brief 获取节点大小 (纯虚函数)
*
* 核心操作之一。派生类必须提供实现。
* - 文件(File): 返回自身大小。
* - 目录(Directory): 递归计算所有子节点大小之和。
*
* @return 节点占用的字节数
*/
virtual int getSize() const = 0;
// =0 表示纯虚函数,使此类成为抽象类
/**
* @brief 添加子节点 (默认实现抛出异常)
*
* 透明模式的体现:方法声明在基类中。
* 对于不支持此操作的叶子节点(File),默认实现是抛出异常。
* 目录(Directory)会重写此方法。
*
* @param child 要添加的子节点的智能指针
* @throws std::runtime_error 当对不支持此操作的节点类型调用时
*/
virtual void addChild(const FileSystemNodePtr& child);
/**
* @brief 移除子节点 (默认实现抛出异常)
* @param child 要移除的子节点的智能指针
* @throws std::runtime_error 当对不支持此操作的节点类型调用时
*/
virtual void removeChild(const FileSystemNodePtr& child);
/**
* @brief 获取指定索引的子节点 (默认实现抛出异常)
* @param index 子节点的索引 (从0开始)
* @return 指向子节点的智能指针
* @throws std::runtime_error 当对不支持此操作的节点类型调用时
* @throws std::out_of_range 当索引超出有效范围时
*/
virtual FileSystemNodePtr getChild(size_t index) const;
/**
* @brief 获取子节点数量 (默认返回0)
*
* 对于叶子节点,总是返回0。
* 目录会重写此方法。
*
* @return 子节点的数量
*/
virtual size_t getChildrenCount() const;
/**
* @brief 以树形格式打印节点信息 (纯虚函数)
*
* 用于演示和调试,直观地展示树形结构。
*
* @param indent 当前行的缩进量,用于实现树形显示
* @param isLast 当前节点是否是父节点的最后一个孩子,用于绘制树线
*/
virtual void print(int indent = 0, bool isLast = true) const = 0;
protected:
std::string m_name;
///< 节点的名称 (protected权限,允许派生类访问)
};
#endif //COMPOSITE_PATTERN_FILE_SYSTEM_NODE_H
include/File.h (一线的“打工人”)
/**
* @file File.h
* @brief 文件类 (Leaf)
*
* 代表文件系统中的一个具体文件,是组合模式中的叶子(Leaf)节点。
* 它不能包含任何子节点。
*/
#ifndef COMPOSITE_PATTERN_FILE_H
#define COMPOSITE_PATTERN_FILE_H
#include "FileSystemNode.h"
class File
: public FileSystemNode {
public:
/**
* @brief 构造函数
* @param name 文件名
* @param size 文件大小(字节)
*/
File(std::string name, int size);
/**
* @brief 获取文件大小
* @return 文件大小(字节)
*/
int getSize() const override;
// C++11: 使用override关键字确保正确重写
/**
* @brief 打印文件信息
* @param indent 缩进量
* @param isLast 是否是最后一个节点(用于绘制树形结构)
*/
void print(int indent = 0, bool isLast = true) const override;
private:
int m_size;
///< 文件的大小(字节)
};
#endif //COMPOSITE_PATTERN_FILE_H
include/Directory.h (劳心劳力的“项目经理”)
/**
* @file Directory.h
* @brief 目录类 (Composite)
*
* 代表文件系统中的一个目录,是组合模式中的复合体(Composite)节点。
* 它可以包含任意数量的子节点(文件或其他目录)。
*/
#ifndef COMPOSITE_PATTERN_DIRECTORY_H
#define COMPOSITE_PATTERN_DIRECTORY_H
#include "FileSystemNode.h"
#include <vector>
class Directory
: public FileSystemNode {
public:
/**
* @brief 构造函数
* @param name 目录名
*/
explicit Directory(std::string name);
// 重写所有子节点管理方法
int getSize() const override;
void addChild(const FileSystemNodePtr& child) override;
void removeChild(const FileSystemNodePtr& child) override;
FileSystemNodePtr getChild(size_t index) const override;
size_t getChildrenCount() const override;
void print(int indent = 0, bool isLast = true) const override;
// 可以添加一些目录特有的方法,比如查找文件等
// FileSystemNodePtr find(const std::string& name) const;
private:
// 使用vector存储子节点,元素类型是基类的智能指针
// 这使得Directory可以持有任何FileSystemNode派生类的对象
std::vector<FileSystemNodePtr> m_children;
///< 子节点列表
};
#endif //COMPOSITE_PATTERN_DIRECTORY_H
4.3 源文件实现 (The Implementation)
src/FileSystemNode.cpp
#include "FileSystemNode.h"
#include <iostream>
// 构造函数初始化成员列表
FileSystemNode::FileSystemNode(std::string name) : m_name(std::move(name)) {
} // 使用std::move优化
const std::string&
FileSystemNode::getName() const {
return m_name;
}
void FileSystemNode::addChild(const FileSystemNodePtr& child) {
// 透明模式的“代价”:叶子节点需要处理不该有的方法
throw std::runtime_error("Cannot add child to a leaf node: " + m_name);
}
void FileSystemNode::removeChild(const FileSystemNodePtr& child) {
throw std::runtime_error("Cannot remove child from a leaf node: " + m_name);
}
FileSystemNodePtr FileSystemNode::getChild(size_t index) const {
(void)index;
// 避免编译器未使用参数的警告
throw std::runtime_error("Cannot get child from a leaf node: " + m_name);
}
size_t FileSystemNode::getChildrenCount() const {
return 0;
// 叶子节点没有孩子,返回0是合理的
}
src/File.cpp
#include "File.h"
#include <iostream>
#include <iomanip>
// 用于std::setw
File::File(std::string name, int size)
: FileSystemNode(std::move(name)), m_size(size) {
} // 初始化基类和自己特有的成员
int File::getSize() const {
return m_size;
}
void File::print(int indent, bool isLast) const {
// 先打印缩进和树线
for (int i = 0; i < indent - 1;
++i) {
std::cout <<
(std::cout.width(2), std::cout <<
"| ");
// 更清晰的树线
}
if (indent >
0) {
std::cout <<
(isLast ? "└─" : "├─");
// 使用Unicode字符让树形更美观
}
// 然后打印节点自身信息
std::cout <<
"[File] " <<
getName() <<
" (" <<
getSize() <<
" bytes)" << std::endl;
}
src/Directory.cpp (核心中的核心)
#include "Directory.h"
#include <iostream>
#include <iomanip>
#include <algorithm>
// 用于std::find
Directory::Directory(std::string name) : FileSystemNode(std::move(name)) {
}
int Directory::getSize() const {
int totalSize = 0;
// 遍历所有子节点
for (const auto& child : m_children) {
// 多态调用:child可能是File或Directory
// 如果是File,调用File::getSize()
// 如果是Directory,调用Directory::getSize(),从而形成递归
totalSize += child->
getSize();
}
return totalSize;
}
void Directory::addChild(const FileSystemNodePtr& child) {
// 简单的实现:直接添加到列表末尾
// 实际应用中可能需要检查重名等
m_children.push_back(child);
}
void Directory::removeChild(const FileSystemNodePtr& child) {
// 使用STL算法查找要删除的子节点
auto it = std::find(m_children.begin(), m_children.end(), child);
if (it != m_children.end()) {
m_children.erase(it);
}
// 如果没找到,可以选择抛出异常 std::out_of_range
// else { throw std::out_of_range("Child not found"); }
}
FileSystemNodePtr Directory::getChild(size_t index) const {
// 检查索引是否越界
if (index >= m_children.size()) {
throw std::out_of_range("Index " + std::to_string(index) + " out of range for directory '" + getName() + "'");
}
return m_children[index];
}
size_t Directory::getChildrenCount() const {
return m_children.size();
}
void Directory::print(int indent, bool isLast) const {
// 打印当前目录节点本身
for (int i = 0; i < indent - 1;
++i) {
std::cout <<
" ";
}
if (indent >
0) {
std::cout <<
(isLast ? "└─" : "├─");
}
std::cout <<
"[Directory] " <<
getName() <<
" (Total: " <<
getSize() <<
" bytes)" << std::endl;
// 递归打印所有子节点
size_t count = m_children.size();
for (size_t i = 0; i < count;
++i) {
const auto& child = m_children[i];
bool childIsLast = (i == count - 1);
// 判断当前子节点是否是最后一个
// 为子节点计算新的缩进量
int newIndent = indent + 2;
// 如果父目录是最后一个,那么缩进的地方应该是空白,否则是竖线
// 这个逻辑可以画图理解,是实现漂亮树形显示的关键
child->
print(newIndent, childIsLast);
}
}
4.4 客户端代码与演示 (The Showtime!)
src/main.cpp
/**
* @file main.cpp
* @brief 组合模式演示客户端
*
* 展示如何使用组合模式构建一个树形文件结构,并统一地进行操作。
*/
#include <iostream>
#include <memory>
// for std::shared_ptr, std::make_shared
#include "File.h"
#include "Directory.h"
// 使用using让类型名更简洁
using FilePtr = std::shared_ptr<File>
;
using DirectoryPtr = std::shared_ptr<Directory>
;
int main() {
std::cout <<
"===== Composite Pattern Demo: File System Simulation =====" << std::endl << std::endl;
// 1. 创建文件和目录 (使用std::make_shared是现代C++的最佳实践)
std::cout <<
"1. Creating files and directories..." << std::endl;
DirectoryPtr rootDir = std::make_shared<Directory>
("Root");
DirectoryPtr documentsDir = std::make_shared<Directory>
("Documents");
DirectoryPtr imagesDir = std::make_shared<Directory>
("Images");
DirectoryPtr musicDir = std::make_shared<Directory>
("Music");
FilePtr readmeFile = std::make_shared<File>
("readme.txt", 100);
FilePtr essayFile = std::make_shared<File>
("essay.docx", 250);
FilePtr photoFile = std::make_shared<File>
("photo.jpg", 1500);
FilePtr songFile = std::make_shared<File>
("song.mp3", 8000);
// 一首很大的歌
// 2. 构建树形结构:组装我们的“文件系统”
std::cout <<
"2. Building the tree structure..." << std::endl;
// 透明性的魔力:所有addChild调用看起来一模一样,尽管参数类型实际不同
documentsDir->
addChild(essayFile);
imagesDir->
addChild(photoFile);
musicDir->
addChild(songFile);
// 向根目录添加节点
rootDir->
addChild(readmeFile);
rootDir->
addChild(documentsDir);
rootDir->
addChild(imagesDir);
rootDir->
addChild(musicDir);
// 添加音乐目录
std::cout <<
" Structure built successfully!" << std::endl << std::endl;
// 3. 统一操作整个结构:展示组合模式的威力
std::cout <<
"3. Operating on the entire structure uniformly..." << std::endl;
std::cout <<
"\n--- Printing the entire file system tree ---" << std::endl;
// 神奇的一刻:只需对根节点调用print,整棵树都会被打印出来
rootDir->
print();
std::cout <<
"\n--- Calculating total size ---" << std::endl;
// 更神奇:计算总大小也是如此简单
std::cout <<
"Total size of root directory: " << rootDir->
getSize() <<
" bytes" << std::endl;
// 4. 演示对任何单一节点的操作也同样简单
std::cout <<
"\n--- Operating on a single leaf node ---" << std::endl;
std::cout <<
"Size of 'readme.txt': " << readmeFile->
getSize() <<
" bytes" << std::endl;
std::cout <<
"\n--- Operating on a branch node ---" << std::endl;
std::cout <<
"Size of 'Documents' directory: " << documentsDir->
getSize() <<
" bytes" << std::endl;
std::cout <<
"Number of children in 'Documents': " << documentsDir->
getChildrenCount() << std::endl;
// 5. 演示透明模式下的“不安全”操作(运行时错误)
std::cout <<
"\n5. Demonstrating the 'cost' of transparency (runtime error)..." << std::endl;
try {
std::cout <<
"Attempting to add a child to a File (readme.txt)..." << std::endl;
readmeFile->
addChild(std::make_shared<File>
("virus.exe", 999));
// 这行会抛出异常
std::cout <<
"Unexpected: This line should not be printed." << std::endl;
} catch (const std::runtime_error& e) {
// 捕获并处理异常
std::cerr <<
" Runtime Error caught: " << e.what() << std::endl;
std::cout <<
" (This is the expected behavior in the transparent approach)" << std::endl;
}
std::cout << std::endl <<
"===== Demo Finished =====" << std::endl;
return 0;
}
4.5 Makefile:一键编译和运行
Makefile
# Compiler and flags
CXX := g++
CXXFLAGS := -std=c++17 -Wall -Wextra -pedantic -I./include -O2
# -std=c++17: 使用C++17标准
# -Wall -Wextra -pedantic: 开启大量警告,帮助写出更健壮的代码
# -I./include: 指定头文件搜索路径
# -O2: 优化级别
# Target executable name
TARGET := composite_demo
# Source files
SRC_DIR := src
SRCS := $(SRC_DIR)/main.cpp $(SRC_DIR)/FileSystemNode.cpp $(SRC_DIR)/File.cpp $(SRC_DIR)/Directory.cpp
# Object files (will be placed in a temporary .objs directory)
OBJ_DIR := .objs
OBJS := $(SRCS:$(SRC_DIR)/%.cpp=$(OBJ_DIR)/%.o)
# e.g., src/main.cpp -> .objs/main.o
# Default target: build the executable
$(TARGET): $(OBJS)
@echo "Linking $@..."
@$(CXX) $(CXXFLAGS) -o $@ $^
@echo "Build successful! Run ./$(TARGET) to execute."
# Compile each .cpp file to a .o file
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)
@echo "Compiling $<..."
@$(CXX) $(CXXFLAGS) -c $< -o $@
# Create the object files directory if it doesn't exist
$(OBJ_DIR):
@mkdir -p $@
# Phony targets (not real files)
.PHONY: all clean run
# `make all` is the same as `make`
all: $(TARGET)
# Clean up generated files
clean:
@echo "Cleaning..."
@rm -rf $(TARGET) $(OBJ_DIR)
@echo "Clean done."
# Run the program
run: $(TARGET)
@./$(TARGET)
# Help message
help:
@echo "Available targets:"
@echo " all - Build the program (default)"
@echo " clean - Remove all build artifacts"
@echo " run - Build and run the program"
@echo " help - Show this help message"
4.6 如何编译、运行及解读结果
编译:
- 确保你有一个现代C++编译器(GCC >= 7, Clang >= 5, MSVC >= 2017)。
- 将上述所有文件放到正确的目录结构中。
- 打开终端,进入
composite_demo目录。 - 输入
make命令。你会看到编译输出,最后显示“Build successful!”。
运行:
- 在终端输入
make run或./composite_demo。 - 程序会开始执行,并在控制台打印出华丽的结果。
- 在终端输入
结果解读:
程序输出会清晰展示以下内容:- 文件系统树形结构: 使用Unicode字符绘制出漂亮的树形图,直观地显示
Root目录下的所有文件和子目录,以及它们的层次关系。 - 统一的大小计算: 分别展示了计算整个根目录、
Documents分支、单个readme.txt文件的大小,证明了对任何节点操作接口的一致性。 - 透明模式的代价: 最后演示了试图向
File添加子节点时抛出的运行时异常,并成功捕获,验证了透明模式的特点。
- 文件系统树形结构: 使用Unicode字符绘制出漂亮的树形图,直观地显示
通过这个完整的例子,你不仅学会了组合模式的原理,更获得了一个可以直接编译、运行、修改和学习的现代C++项目模板。
5. 总结与升华
组合模式是一个极其强大且应用广泛的结构型模式。它通过“部分-整体”的层次结构和统一的操作接口,将复杂树形结构的操作简化到了极致。
它的核心价值在于让客户端代码摆脱了对复杂对象内部结构的依赖,使其只需要面对一个统一的抽象接口,从而极大地提高了代码的简洁性、可维护性和可扩展性。
它的实现关键在于区分清楚Leaf和Composite的角色,并处理好透明性与安全性的权衡。
它的灵魂是递归和委托,
Composite对象将请求委托给其子组件,子组件再继续委托,直到Leaf对象完成实际工作。
希望这篇超过30000字的详尽解析,能让你对组合模式的理解上升到一个全新的高度。现在,就打开你的代码编辑器,尝试用组合模式去重构或设计那些充满层次结构的模块吧!你会发现,你的代码世界将因此而变得更加清晰和优雅。

浙公网安备 33010602011771号