C# 设计模式:组合模式 (Composite Pattern)
组合模式(Composite Pattern)是一种结构型设计模式。它允许你将对象组合成树形结构来表现“整体/部分”的层次结构。组合能让客户端以一致的方式处理个别对象(叶子)和对象组合(容器)。
1. 核心概念
在 C# 中实现组合模式,主要涉及三个角色:
- Component (抽象构件): 这是一个接口或抽象类,为组合中的对象声明接口。它实现了所有类共有接口的默认行为。
- Leaf (叶子节点): 在组合中表示叶子节点对象,叶子节点没有子节点。它实现了 Component 中的业务方法。
- Composite (容器节点): 定义有子部件的那些部件的行为。它存储子部件,并实现在 Component 接口中定义的与子部件有关的操作(如 Add, Remove)。
2. 操作步骤与代码实现
我们以“文件系统”为例:文件夹(Composite)可以包含文件(Leaf),也可以包含其他文件夹。
第一步:定义抽象构件 (Component)
这是所有对象的基类。为了保证“透明性”,我们通常在这里定义管理子节点的方法(Add/Remove),尽管叶子节点可能不需要它们。
using System; using System.Collections.Generic; namespace CompositePatternDemo { // 抽象构件 public abstract class FileSystemItem { protected string _name; public FileSystemItem(string name) { _name = name; } // 核心业务方法:所有节点都有这个功能 public abstract void Display(int depth); // 管理子节点的方法(虚方法,默认抛出异常,由容器节点重写) // 这种方式称为“透明方式”,客户端不需要区分是叶子还是容器 public virtual void Add(FileSystemItem item) { throw new NotImplementedException("叶子节点不支持添加子项"); } public virtual void Remove(FileSystemItem item) { throw new NotImplementedException("叶子节点不支持移除子项"); } } }
第二步:定义叶子节点 (Leaf)
叶子节点代表具体的实体(如文件)。它不包含子节点列表。
namespace CompositePatternDemo { // 叶子节点:文件 public class File : FileSystemItem { public File(string name) : base(name) { } public override void Display(int depth) { // 通过连字符显示层级深度 Console.WriteLine(new String('-', depth) + " File: " + _name); } // File 不需要重写 Add 和 Remove,因为继承的默认行为是抛出异常,符合逻辑 } }
第三步:定义容器节点 (Composite)
容器节点包含一个子节点列表,并实现了管理子节点的方法。关键点在于它的业务方法通常会递归调用子节点的业务方法。
namespace CompositePatternDemo { // 容器节点:文件夹 public class Directory : FileSystemItem { // 内部维护一个子组件列表 private List<FileSystemItem> _children = new List<FileSystemItem>(); public Directory(string name) : base(name) { } // 重写 Add 方法 public override void Add(FileSystemItem item) { _children.Add(item); } // 重写 Remove 方法 public override void Remove(FileSystemItem item) { _children.Remove(item); } // 核心:递归调用 public override void Display(int depth) { // 1. 显示自己 Console.WriteLine(new String('-', depth) + " Directory: " + _name); // 2. 遍历并显示所有子节点(递归) foreach (var component in _children) { component.Display(depth + 2); } } } }
第四步:客户端调用
客户端代码不需要关心它处理的是简单的 `File` 还是复杂的 `Directory`,它只需要针对 `FileSystemItem` 编程。
class Program { static void Main(string[] args) { // 1. 创建根节点 Directory root = new Directory("Root"); // 2. 创建叶子节点 File file1 = new File("Config.xml"); File file2 = new File("Logo.png"); // 3. 创建子容器 Directory subDir = new Directory("Bin"); File subFile = new File("App.exe"); // 4. 组装树形结构 root.Add(file1); root.Add(file2); subDir.Add(subFile); // 向子文件夹添加文件 root.Add(subDir); // 将子文件夹添加到根目录 // 5. 操作整体 // 客户端只需要调用根节点的 Display,整个树结构会自动递归处理 Console.WriteLine("文件系统结构图:"); root.Display(1); } }
3. 两种实现方式的权衡:透明 vs 安全
在 C# 实现中,关于 `Add` 和 `Remove` 方法放在哪里,有两种流派:
- 透明方式 (Transparency):
- 操作: 如上例所示,在抽象类 `Component` 中声明 `Add`/`Remove`。
- 优点: 客户端不需要区分是叶子还是容器,调用一致。
- 缺点: 叶子节点调用 `Add` 会在运行时报错(不够安全)。
- 安全方式 (Safety):
- 操作: 在 `Component` 中不声明 `Add`/`Remove`,只在 `Composite` 类中声明。
- 优点: 编译期就能检查错误,叶子节点根本没有 `Add` 方法。
- 缺点: 客户端必须区分对待,如果想添加子节点,必须先将对象强制转换为 `Composite` 类型。
4. 适用场景
- BIM 开发: 如“筏板(Raft)”包含“电梯井(Pit)”,“楼层”包含“墙/柱/梁”。
- UI 控件树: WinForms 或 WPF 中,Panel 可以包含 Button,也可以包含其他 Panel。
- 图形编辑器: 一个“分组”可以包含圆形、矩形,也可以包含另一个“分组”。
5. 总结
组合模式的核心在于利用多态和递归。通过将容器和叶子抽象为同一类型,使得极其复杂的树形结构在客户端看来只是一个简单的对象。