本节介绍的是结构型模式中的组合模式。
1. 模式概述
组合模式(Composite Pattern) 是一种结构型设计模式,用于将对象组合成树形结构 以表示"部分-整体"的层次结构。组合模式使得客户端对单个对象和组合对象的使用具有一致性,可以像处理单个对象一样处理对象集合。
2. 模式结构
主要角色:
-
组件(Component):定义组合中所有对象的通用接口
-
叶子(Leaf):表示组合中的叶子节点对象,没有子节点
-
容器(Composite):表示组合中的容器节点对象,可以包含子节点
-
客户端(Client):通过组件接口操作组合中的对象
3. 基本示例:文件系统
文件系统是组合模式最常见的应用,在作为客户端的控制台展现的文件树中,我们可以清楚的看到
-
被用作容器的文件夹
-
以及视作叶子的各类文件
而文件系统项抽象类则是组件。
(1)抽象组件
C#
// 文件系统项抽象类
public abstract class FileSystemItem
{
// 文件系统项名称
public string? Name { get; protected set; }
// 文件系统项大小
public long Size { get; protected set; }
// 构造函数
public FileSystemItem(string name)
{
Name = name;
}
// 显示文件系统项信息
public abstract void Display(int depth);
// 获取文件系统项大小
public abstract long GetSize();
// 添加子项
public virtual void Add(FileSystemItem item)
{
throw new NotSupportedException("无法将项添加到此FileSystemItem类型中。");
}
// 移除子项
public virtual void Remove(FileSystemItem item)
{
throw new NotSupportedException("无法从此FileSystemItem类型中删除项。");
}
// 获取子项
public virtual FileSystemItem? GetChild(int index)
{
throw new NotSupportedException("此FileSystemItem类型不包含子项。");
}
// 获取缩进字符串
protected string GetIndentation(int depth)
{
return new string('-', depth * 2);
}
}
(2)叶子节点 - 文件类
C#
/// <summary>
/// 表示文件系统中的一个文件,提供对其名称和大小的访问。
/// </summary>
public class ExFile : FileSystemItem
{
/// <summary>
/// 使用指定的名称和大小初始化 ExFile 类的新实例。
/// </summary>
/// <param name="name">文件名</param>
/// <param name="size">文件大小</param>
public ExFile(string name,long size) : base(name)
{
Size = size;
}
/// <summary>
/// 在控制台中显示文件的名称和大小,并根据指定的深度进行缩进格式化。
/// </summary>
/// <param name="depth">显示文件时应用的缩进级别。必须大于或等于0。根级别为0</param>
public override void Display(int depth = 0)
{
Console.WriteLine($"{GetIndentation(depth)}[文件]{Name}({Size}bytes)");
}
/// <summary>
/// 获取文件大小。
/// </summary>
/// <returns>文件大小(单位:byte)</returns>
public override long GetSize() => Size;
}
(3)容器节点 - 文件夹类
C#
/// <summary>
/// 表示文件系统中的一个文件夹,该文件夹可以包含文件和其他文件夹。
/// </summary>
/// <remarks>文件夹是一个复合文件系统项,可以包含多个子项,包括文件和子文件夹。
/// 使用提供的方法可以添加、删除和枚举其中的项。
/// 文件夹的大小计算为其子项大小的总和。
/// 此类通常用于构建层次化的文件系统结构。</remarks>
public class ExFolder : FileSystemItem
{
// 子项集合
private List<FileSystemItem> _items = [];
/// <summary>
/// 使用指定名称初始化Folder类的新实例。
/// </summary>
/// <param name="name">文件夹名称</param>
public ExFolder(string name) : base(name)
{
Size = 0;
}
/// <summary>
/// 在控制台中显示当前文件夹及其内容,使用缩进表示文件夹的层级结构。
/// </summary>
/// <remarks>此方法递归显示文件夹中包含的所有项目,每嵌套一层就增加一层缩进。输出内容包括文件夹名称及其总大小(以字节为单位)。</remarks>
/// <param name="depth">要应用的缩进级别,表示文件夹在层级结构中的深度。必须为零或大于零。</param>
public override void Display(int depth = 0)
{
Console.WriteLine($"{GetIndentation(depth)}[文件夹]{Name}(共{Size}bytes)");
foreach (var item in _items)
{
item.Display(depth + 1);
}
}
/// <summary>
/// 获取当前文件夹及其所有子项的总大小。
/// </summary>
public override long GetSize()
{
long totalSize = 0;
foreach (var item in _items)
{
totalSize += item.GetSize();
}
return totalSize;
}
/// <summary>
/// 添加一个文件系统项到当前文件夹中。
/// </summary>
public override void Add(FileSystemItem item)
{
_items.Add(item);
Size += item.GetSize();
}
/// <summary>
/// 移除当前文件夹中的一个文件系统项。
/// </summary>
public override void Remove(FileSystemItem item)
{
_items.Remove(item);
Size -= item.GetSize();
}
/// <summary>
/// 基于索引位置获取当前文件夹中的一个子项。
/// </summary>
public override FileSystemItem? GetChild(int index)
{
if (index < 0 || index >= _items.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), "索引超出范围。");
}
return _items[index];
}
/// <summary>
/// 获取当前文件夹中子项的数量。
/// </summary>
public int GetItemCount()
{
return _items.Count;
}
}
(4)客户端实现
C#
Console.WriteLine("Hello, World!");
Console.WriteLine("=== 文件系统示例 ===");
// 创建示例文件
ExFile file1 = new ExFile("文件1.txt", 1200);
ExFile file2 = new ExFile("文件2.jpg", 3400);
ExFile file3 = new ExFile("文件3.docx", 5600);
ExFile file4 = new ExFile("文件4.mp4", 7800);
// 创建文件夹
ExFolder folder1 = new ExFolder("文件夹1");
ExFolder folder2 = new ExFolder("文件夹2");
ExFolder rootFolder = new ExFolder("根文件夹");
// 构建文件系统层次结构
folder1.Add(file1);
folder1.Add(file2);
folder2.Add(file3);
rootFolder.Add(folder1);
rootFolder.Add(folder2);
rootFolder.Add(file4);
rootFolder.Add(folder2);
// 显示文件系统结构
Console.WriteLine("\n文件系统结构:");
rootFolder.Display();
// 计算并显示根文件夹的总大小
Console.WriteLine($"\n根文件夹总大小: {rootFolder.GetSize()} bytes");
运行结果如下:
bash
Hello, World!
=== 文件系统示例 ===
文件系统结构:
[文件夹]根文件夹(共23600bytes)
--[文件夹]文件夹1(共4600bytes)
----[文件]文件1.txt(1200bytes)
----[文件]文件2.jpg(3400bytes)
--[文件夹]文件夹2(共5600bytes)
----[文件]文件3.docx(5600bytes)
--[文件]文件4.mp4(7800bytes)
--[文件夹]文件夹2(共5600bytes)
----[文件]文件3.docx(5600bytes)
根文件夹总大小: 23600 bytes
4. 组合模式的优势
- 优点:
-
简化客户端代码:客户端可以一致地处理单个对象和组合对象
-
更容易添加新类型的组件:添加新的叶子或容器类不会影响现有代码
-
定义了包含基本对象和组合对象的类层次结构:可以更灵活地构建复杂结构
-
可以方便地遍历整个组合结构:便于执行批量操作
- 缺点:
-
设计较为抽象:需要正确识别应用场景
-
限制了类型约束:组合模式中的组件类型是相同的,可能不够类型安全
-
使设计变得更加一般化:有时难以限制组合中的组件
5. 适用场景
-
表示对象的部分-整体层次结构
-
文件系统(文件夹和文件)
-
组织架构(部门和员工)
-
图形系统(复合图形和简单图形)
-
希望客户端忽略组合对象与单个对象的差异
-
客户端统一对待所有对象
-
需要对对象集合执行统一操作
-
批量操作(如移动、删除、计算总大小)
-
6. 实际应用建议
-
最佳实践:
-
明确定义组件接口:确保接口适合所有具体类
-
在适当位置实现默认行为:在抽象类中提供默认实现
-
保持叶子节点简单:避免在叶子节点中实现不必要的容器方法
-
考虑使用透明式组合模式:
csharp
// 透明式:在抽象类中声明所有方法(包括管理子组件的方法)
// 安全式:只在容器类中声明管理子组件的方法
透明式 vs 安全式:
csharp
// 透明式组合模式(推荐)
public abstract class Component
{
// 公共操作
public abstract void Operation();
// 子组件管理(透明)
public virtual void Add(Component c) { /* 默认实现 */ }
public virtual void Remove(Component c) { /* 默认实现 */ }
public virtual Component GetChild(int i) { return null; }
}
// 安全式组合模式
public abstract class Component
{
// 只包含公共操作
public abstract void Operation();
}
public class Composite : Component
{
// 容器特有的方法
public void Add(Component c) { }
public void Remove(Component c) { }
public Component GetChild(int i) { return null; }
public override void Operation() { }
}
7. 与其他模式的关系
与装饰器模式:两者都使用递归组合,但装饰器模式为对象添加职责,而组合模式创建对象树
与迭代器模式:可以结合使用迭代器模式来遍历组合结构
与访问者模式:可以结合使用访问者模式对组合结构中的所有元素执行操作
8. 总结
组合模式通过树形结构组织对象,实现了部分-整体层次结构的表示。它使得客户端可以一致地处理单个对象和组合对象,简化了复杂结构的操作和维护。在实际应用中,组合模式特别适合需要构建层次化对象系统的场景。