你是否曾经把 App 界面的主题从浅色整体切换为深色?
如果逐个替换按钮和标签的颜色,总会漏掉某个地方。很容易出现按钮是黑色背景,而标签仍然保留白色背景的问题。
这正是 Swift 抽象工厂模式适用的场景。将相关对象组合成一个集合,再整体替换。
修改单个对象创建位置的模式,与创建整个产品族的模式,适用范围不同。如果想先划清边界,请同时阅读 工厂方法与抽象工厂的区别。
本文将总结抽象工厂模式究竟解决什么问题、如何使用 Swift 代码整体替换相关对象,以及什么时候适合或不适合使用它。
什么是抽象工厂模式?
用一句话回答就是:
根据具体情况,整体替换一个工厂,而这个工厂会成组创建必须相互兼容的对象。
关键在于“必须一起使用的对象”。把深色主题的按钮和浅色主题的背景混在一起,就会产生问题。
抽象工厂可以避免这类组合错误。
选定一个工厂后,它生产的部件会自动相互匹配。选择浅色工厂,就会得到浅色按钮、浅色标签和浅色背景。
不是逐个选择部件,而是直接选择整个工厂。
这就是抽象工厂的全部含义。
在 Swift 中整体替换相关对象的方法
结合实际代码会清晰得多。下面以 UI 主题为例。
首先定义待创建部件的规范(协议),以及生产这些部件的工厂规范。
protocol Button { func render() -> String }
protocol Label { func render() -> String }
// 工厂负责整套相互匹配的部件
protocol ThemeFactory {
func makeButton() -> Button
func makeLabel() -> Label
}
现在分别创建浅色主题工厂和深色主题工厂。每个工厂只生产符合自身主题的部件。
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 的项目中,或纯逻辑层需要替换整套对象时,抽象工厂更能发挥作用。
抽象工厂归根结底是一种“用同一个把手绑定不能各自运行的事物”的模式。
当你体验过只改一行主题就让整个界面完全协调起来,就会立刻明白为什么要使用这个模式。建议先从一个小例子开始亲手实现。

