软件设计

Swift Composite Pattern:将树形结构当作一个对象处理

文件夹里有文件夹,里面还有文件。如果你曾经用代码处理过这种树形结构,应该至少有过一次不知所措。

4 分钟阅读
Swift Composite Pattern:将树形结构当作一个对象处理 封面图

文件夹里有文件夹,里面还有文件。如果你曾经用代码处理过这种树形结构,应该至少有过一次不知所措。

每次都用if区分“这是文件夹还是文件?”,代码很快就会变得杂乱。

制作类似文件浏览器的功能时,这是很常见的困境。这时可以使用的工具,就是 Composite Pattern。

Composite Pattern 将单个对象(文件)和对象集合(文件夹)视为同一种类型,从而可以像处理一个对象一样处理整棵树。

今天就结合我亲自编写的代码,整理一下如何用 Swift 实现这个模式。

先看核心总结

  1. Composite Pattern = 用同一个协议统一 Leaf 和 Composite
  2. 客户端无需区分文件还是文件夹,直接以相同方式调用
  3. 文件夹的计算会再次询问子对象,形成递归结构
  4. 在 Swift 中只需一个protocol即可简洁实现

什么是 Composite Pattern?

顾名思义,就是“组合”:将小的对象聚合成一个更大的整体。

最常见的例子就是文件系统。

一个文件本身就是终点,也就是叶节点。而文件夹则是可以容纳文件或其他文件夹的分支。

文件夹里还有文件夹,这正是 Composite
文件夹里还有文件夹,这正是 Composite

不过,用户的视角有所不同。无论打开文件夹还是文件,我们都只是发出“打开”这个命令。

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()即可,剩下的由各个对象自行处理。

我把示例原样粘贴到 Playground 中运行了
我把示例原样粘贴到 Playground 中运行了

和使用分支语句处理有什么不同?

你可能会想:“直接用if语句区分类型来处理,不行吗?”

可以。不过树越深,差异就越明显。

分类 分支语句(if/switch)方式 Composite Pattern
类型判断 每次手动确认 不需要
添加新类型 修改各处的分支语句 采用协议
递归处理 手动编写遍历代码 对象自行处理
代码可读性 越深越复杂 保持一致

使用分支方式时,如果新增一种类型(例如快捷链接),就需要在各处再次添加if

而使用 Composite,只需创建一个采用FileComponent的新类型即可,无需修改现有代码。


常见问题

Q. Leaf 和 Composite 的接口必须完全相同吗?

理想情况下是的。不过,“添加子对象”之类的功能对文件来说并不自然。这时可以只把公共行为(如 size)放进协议,并仅在文件夹中管理子对象。

Q. 可以不用class,改用struct吗?

可以,像上面的示例一样,使用值类型也能正常运行。不过,如果经常修改树并需要共享引用,class可能更方便。


如果你在树形结构面前陷入if地狱,不妨试试 Composite Pattern。

它不仅适用于文件系统,也适用于视图层级、菜单、组织结构图等“里面还有内容”的结构。

把今天的示例原样粘贴到 Playground 中运行,很快就能理解这个概念。

延伸阅读