软件设计

Swift Template Method Pattern:使用协议扩展搭建骨架

你是否曾在不同类中复制粘贴相似逻辑,并想着“这好像不太对”?在 iOS 应用里,当你第三次复制某个页面的数据加载代码时,手就会停下来。

4 分钟阅读
Swift Template Method Pattern:使用协议扩展搭建骨架 封面图

你是否曾在不同类中复制粘贴相似逻辑,并想着“这好像不太对”?在 iOS 应用里,当你第三次复制某个页面的数据加载代码时,手就会停下来。

这时,Template Method Pattern 就派上用场了。

简单来说,这种模式把完整流程(骨架)固定在一个地方,只让各个实现填充不同的步骤。在 Swift 中,使用协议扩展比继承更简洁。

本文将依次介绍 Template Method Pattern 是什么、为什么在 Swift 中协议扩展更接近理想方案,以及如何用实际代码搭建骨架。

先来看看核心总结

  1. Template Method Pattern 将“固定流程 + 可替换步骤”分离开来
  2. 传统做法是通过父类继承实现,但在 Swift 中协议扩展更合适
  3. 把骨架放在扩展的默认实现中,把变化部分保留为协议要求
  4. 摆脱单继承、强耦合等继承限制

什么是 Template Method Pattern?

名字听起来很复杂,但概念很简单。

想象一下菜谱。“准备食材 → 处理 → 烹饪 → 装盘”的顺序是固定的。

但食材和烹饪方式会因菜品而不同。

这里,顺序(骨架)就是 Template Method,而每道菜不同的步骤就是可替换部分。

在一个地方控制流程,只把具体实现委托到外部。

这句话基本可以概括 Template Method Pattern 的全部内容。

从代码来看,情况是这样的。每个页面的数据加载流程都相同:开始加载 → 请求 → 解析 → 更新页面。

不同的只是“请求哪个 URL”和“如何解析收到的数据”。

每次重新编写这套通用流程,都是一种浪费。


Swift 中为什么不用继承,而使用协议扩展?

传统方式是在父类中放置骨架,再让子类重写特定方法。就是 Java 教材里的那种方式。

但在 Swift 中通过类继承实现,会遇到不少限制。

  • 类只能单继承。如果已经有一个父类,就无法再继承其他父类
  • 完全不能用于 struct 或 enum
  • 父子类高度耦合,之后很难替换

协议扩展可以解决其中大部分问题。

把骨架方法作为扩展的默认实现提供,把变化的步骤只声明为协议要求。

这样,无论是 struct 还是 class,只要遵循该协议,就能免费获得通用骨架,不受继承限制。

下面是用协议定义数据加载流程的示例。load()是固定骨架,另外两个是可替换部分。

protocol DataLoadable {
    associatedtype Item
    var endpoint: URL { get }          // 每个页面不同
    func parse(_ data: Data) -> [Item] // 每个页面不同
}

extension DataLoadable {
    func load() async throws -> [Item] {   // 固定骨架
        let (data, _) = try await URLSession.shared.data(from: endpoint)
        return parse(data)
    }
}

load()的顺序由扩展控制。

每个页面只需按自己的方式实现endpoint和parse。

骨架放在协议中,遵循协议的一方填充空白
骨架放在协议中,遵循协议的一方填充空白
固定骨架,只替换变化的步骤
固定骨架,只替换变化的步骤

实际搭建一下骨架

现在来看看遵循这个协议的一方。因为是遵循而不是继承,所以也可以使用 struct。

struct ArticleLoader: DataLoadable {
    let endpoint = URL(string: "https://api.example.com/articles")!
    func parse(_ data: Data) -> [Article] {
        (try? JSONDecoder().decode([Article].self, from: data)) ?? []
    }
}
// 使用: let articles = try await ArticleLoader().load()

如你所见,ArticleLoader没有直接实现load()。

即便如此,它仍然可以调用load(),因为扩展提供了默认实现。

这就是使用协议扩展构建 Template Method 的力量。新增页面时,只需再创建一个实现endpoint和parse的 struct 即可。

改用这种方式后,我把新增页面的代码减少到了一半以下。加载和错误处理逻辑集中到一处后,修复 bug 也容易多了。

新增页面只需再创建一个 struct,代码大幅减少
新增页面只需再创建一个 struct,代码大幅减少

需要注意一点:协议扩展的默认实现与 override 的行为略有不同。即使遵循协议的类型定义了同名方法,如果没有将其声明为协议要求,静态分派也可能调用错误的实现。

因此,最好务必将可替换步骤声明为协议正文中的要求。


用简短的问答做个总结

问:那现在可以不用基于继承的 Template Method 了吗?

在 UIKit 这类已经存在类层次结构的地方,通过继承进行 override 有时仍然很自然。根据具体情况选择即可。

问:associatedtype 让我觉得有些复杂。

如果返回类型固定,可以不使用 associatedtype,直接定义普通协议。只有需要泛型时才使用它。


试着用协议扩展搭建骨架后,代码会整理得如此清晰,以至于之前与继承苦苦周旋的经历都显得有些徒劳。

如果你现在有一段不断复制粘贴的逻辑,今天就挑一段提取到协议中吧。很快就能体会其中的好处。加油!

延伸阅读