软件设计

Swift 抽象工厂模式:一次替换整组相关对象

抽象工厂会将多个彼此兼容的对象组成产品族,并一次性完成替换。本文以 Swift 主题为例,说明如何避免组合错误,以及如何在抽象工厂和工厂方法之间进行选择。

4 分钟阅读
Swift 抽象工厂模式:一次替换整组相关对象 封面图

你是否曾经把 App 界面的主题从浅色整体切换为深色?

如果逐个替换按钮和标签的颜色,总会漏掉某个地方。很容易出现按钮是黑色背景,而标签仍然保留白色背景的问题。

这正是 Swift 抽象工厂模式适用的场景。将相关对象组合成一个集合,再整体替换。

修改单个对象创建位置的模式,与创建整个产品族的模式,适用范围不同。如果想先划清边界,请同时阅读 工厂方法与抽象工厂的区别

本文将总结抽象工厂模式究竟解决什么问题、如何使用 Swift 代码整体替换相关对象,以及什么时候适合或不适合使用它。


什么是抽象工厂模式?

用一句话回答就是:

根据具体情况,整体替换一个工厂,而这个工厂会成组创建必须相互兼容的对象。

关键在于“必须一起使用的对象”。把深色主题的按钮和浅色主题的背景混在一起,就会产生问题。

抽象工厂可以避免这类组合错误。

选定一个工厂后,它生产的部件会自动相互匹配。选择浅色工厂,就会得到浅色按钮、浅色标签和浅色背景。

不是逐个选择部件,而是直接选择整个工厂。

这就是抽象工厂的全部含义。


在 Swift 中整体替换相关对象的方法

结合实际代码会清晰得多。下面以 UI 主题为例。

首先定义待创建部件的规范(协议),以及生产这些部件的工厂规范。

protocol Button { func render() -> String }
protocol Label  { func render() -> String }

// 工厂负责整套相互匹配的部件
protocol ThemeFactory {
    func makeButton() -> Button
    func makeLabel() -> Label
}
一切都从定义一个 protocol 开始
一切都从定义一个 protocol 开始

现在分别创建浅色主题工厂和深色主题工厂。每个工厂只生产符合自身主题的部件。

选定一个工厂,部件会自动成套提供
选定一个工厂,部件会自动成套提供
struct LightFactory: ThemeFactory {
    func makeButton() -> Button { LightButton() }
    func makeLabel() -> Label  { LightLabel() }
}
struct DarkFactory: ThemeFactory {
    func makeButton() -> Button { DarkButton() }
    func makeLabel() -> Label  { DarkLabel() }
}

绘制界面的代码只需要接收一个工厂,不必关心具体是哪一个工厂。

func buildScreen(with factory: ThemeFactory) {
    let button = factory.makeButton()
    let label  = factory.makeLabel()
    print(button.render(), label.render())
}

// 切换主题只需修改工厂中的一行代码
buildScreen(with: DarkFactory())

LightFactory()替换为 DarkFactory()的瞬间,按钮和标签就会一起切换为深色。

再也不需要分别修改按钮和标签的颜色了。

从浅色到深色,只改一行界面就完全协调了
从浅色到深色,只改一行界面就完全协调了

它和工厂方法有什么不同?

名称很相似,容易混淆,下面说明两者的区别。

区分 工厂方法 抽象工厂
创建对象 一种对象 多种相关对象(一个集合)
关注点 “创建什么?” “将相互匹配的对象组合起来”
典型示例 创建一个按钮 按钮、标签和背景作为一套

简单来说,工厂方法是创建一个部件的方法,而抽象工厂是将这些部件组合成一套的方法。

也可以把它理解为:抽象工厂内部使用了多个工厂方法。


什么时候使用,什么时候避免?

实际使用后,我发现适合的场景和反而过度设计的场景区分得很明显。

适合的场景

  • 当主题、平台、国家等明确以“整套”为单位变化时
  • 当需要像 iOS 和 macOS 那样整体替换 UI 部件族时
  • 当支付方式、数据库驱动等需要成套替换时

这些情况会过度设计

  • 只需要创建一两种对象时
  • 组合规则经常变化,导致集合本身不稳定时

明明只有一个对象,却建立抽象工厂,反而会增加代码并降低可读性。

在有明确理由将对象组合成一套时再使用它比较好。


两个常见问题

Q. 之后想新增部件类型怎么办?

需要向协议中添加方法,并在所有工厂中实现该方法。工厂较多时会很费力,因此如果项目中的部件类型经常增加,最好提前考虑这一点。

Q. SwiftUI 中也会使用吗?

当然可以,但 SwiftUI 已经提供了 Environment@Observable 这类内置注入方式,因此只处理主题时,它们通常更简洁。在基于 UIKit 的项目中,或纯逻辑层需要替换整套对象时,抽象工厂更能发挥作用。


抽象工厂归根结底是一种“用同一个把手绑定不能各自运行的事物”的模式。

当你体验过只改一行主题就让整个界面完全协调起来,就会立刻明白为什么要使用这个模式。建议先从一个小例子开始亲手实现。

延伸阅读