文件夹里有文件夹,里面还有文件。如果你曾经用代码处理过这种树形结构,应该至少有过一次不知所措。
每次都用if区分“这是文件夹还是文件?”,代码很快就会变得杂乱。
制作类似文件浏览器的功能时,这是很常见的困境。这时可以使用的工具,就是 Composite Pattern。
Composite Pattern 将单个对象(文件)和对象集合(文件夹)视为同一种类型,从而可以像处理一个对象一样处理整棵树。
今天就结合我亲自编写的代码,整理一下如何用 Swift 实现这个模式。
先看核心总结
- Composite Pattern = 用同一个协议统一 Leaf 和 Composite
- 客户端无需区分文件还是文件夹,直接以相同方式调用
- 文件夹的计算会再次询问子对象,形成递归结构
- 在 Swift 中只需一个
protocol即可简洁实现
什么是 Composite Pattern?
顾名思义,就是“组合”:将小的对象聚合成一个更大的整体。
最常见的例子就是文件系统。
一个文件本身就是终点,也就是叶节点。而文件夹则是可以容纳文件或其他文件夹的分支。
不过,用户的视角有所不同。无论打开文件夹还是文件,我们都只是发出“打开”这个命令。
Composite Pattern 的目标,就是让单个元素和对象集合能够用相同的方式处理。
组件分为三类。
- Component:公共接口(协议)
- Leaf:没有子对象的末端对象(文件)
- Composite:容纳子对象的对象(文件夹)
如何用 Swift 实现?
首先定义公共协议,让我们可以获取名称和大小。
下面是文件和文件夹共同遵循的规则(Component)。
protocol FileComponent {
var name: String { get }
func size() -> Int // 返回字节大小
}
接下来是作为叶节点的文件。只需返回自身大小,非常简单。
struct File: FileComponent {
let name: String
let bytes: Int
func size() -> Int { bytes } // 原样返回自身大小
}
关键在于文件夹(Composite)。它用数组保存子对象,收到大小请求时会再次询问子对象。
struct Folder: FileComponent {
let name: String
var children: [FileComponent] = []
func size() -> Int {
children.reduce(0) { $0 + $1.size() } // 子对象求和(递归)
}
}
这里的关键是,size()会再次调用自身,形成递归结构。
无论文件夹嵌套多少层,这一行都会自动遍历到底层。
实际使用起来是这样
现在来混合文件和文件夹,创建一棵树。
let root = Folder(name: "文档", children: [
File(name: "备忘录.txt", bytes: 100),
Folder(name: "照片", children: [
File(name: "旅行.jpg", bytes: 2000)
])
])
print(root.size()) // 2100 输出
看到了吗?调用一次root.size(),就能汇总文件夹中所有文件的大小。
这是我亲自运行后最感到惊喜的部分。
客户端代码一次都不需要询问“这是文件夹还是文件”。
只要调用size()即可,剩下的由各个对象自行处理。
和使用分支语句处理有什么不同?
你可能会想:“直接用if语句区分类型来处理,不行吗?”
可以。不过树越深,差异就越明显。
| 分类 | 分支语句(if/switch)方式 | Composite Pattern |
|---|---|---|
| 类型判断 | 每次手动确认 | 不需要 |
| 添加新类型 | 修改各处的分支语句 | 采用协议 |
| 递归处理 | 手动编写遍历代码 | 对象自行处理 |
| 代码可读性 | 越深越复杂 | 保持一致 |
使用分支方式时,如果新增一种类型(例如快捷链接),就需要在各处再次添加if。
而使用 Composite,只需创建一个采用FileComponent的新类型即可,无需修改现有代码。
常见问题
Q. Leaf 和 Composite 的接口必须完全相同吗?
理想情况下是的。不过,“添加子对象”之类的功能对文件来说并不自然。这时可以只把公共行为(如 size)放进协议,并仅在文件夹中管理子对象。
Q. 可以不用class,改用struct吗?
可以,像上面的示例一样,使用值类型也能正常运行。不过,如果经常修改树并需要共享引用,class可能更方便。
如果你在树形结构面前陷入if地狱,不妨试试 Composite Pattern。
它不仅适用于文件系统,也适用于视图层级、菜单、组织结构图等“里面还有内容”的结构。
把今天的示例原样粘贴到 Playground 中运行,很快就能理解这个概念。

