软件设计

Swift 工厂方法模式(Factory Method Pattern):为什么不用 init

Swift 工厂函数会为创建逻辑赋予表达意图的名称,并让返回类型和复用策略更加灵活。本文总结选择 static func 而不是 init 的标准,以及它与 GoF 工厂方法的区别。

3 分钟阅读
Swift 工厂方法模式(Factory Method Pattern):为什么不用 init 封面图

使用 Swift 开发应用时,你每天往往要写几十次对象创建代码。

但到了某个阶段,只使用 init 会越来越受限。

很多人一开始什么都用 init,随着项目变大,便会开始了解工厂方法模式。

本文结合实际代码,讲解 Swift 工厂方法模式是什么,以及为什么用工厂代替 init。

一句话说明工厂方法模式

先直接说结论。

工厂方法会把对象创建逻辑移到 init 外部的独立方法中,为它命名,并自由选择返回类型。

在 Swift 中,通常指通过 static func 创建的静态工厂方法。

本文讨论的是通过类型方法封装创建过程的静态工厂。它不同于由子类决定创建类型的 GoF 模式;两者的边界已在工厂方法 vs. 抽象工厂中进行比较。

init 只能在语言规定的规则内运行。

它不能自定义名称,而且必须始终创建自身类型的新实例。

工厂方法解除了这些限制。

来看一个简单的例子。

struct Color {
    let r, g, b: Double
    // 通过名称表达创建意图
    static func rgb(_ r: Double, _ g: Double, _ b: Double) -> Color {
        Color(r: r, g: g, b: b)
    }
    static func gray(_ v: Double) -> Color {
        Color(r: v, g: v, b: v)
    }
}

Color.gray(0.5)这样调用时,只看代码也能知道它会创建灰色。


为什么用工厂代替 init?

根据我的实际经验,主要有四个原因。

第一,可以为它命名。

所有 init 的名称都相同,只能通过参数来区分。

如果有多个接收两个Double的构造器,很容易让人困惑。

工厂方法可以通过from(hex:)rgb(_:_:_:)这样的名称表达意图。

第二,不必每次都创建新实例。

init 总会返回新对象,而工厂可以返回缓存值或单例。

第三,可以灵活选择返回类型。

根据情况,它可以返回子类型或其他实现。

第四,可以更清晰地表达失败处理。

只加一行 static func,代码就会更易读
只加一行 static func,代码就会更易读

下面是一个使用缓存的工厂示例。

final class IconCache {
    private static var store: [String: Icon] = [:]
    // 名称相同时复用已创建的图标
    static func icon(named name: String) -> Icon {
        if let cached = store[name] { return cached }
        let icon = Icon(name: name)
        store[name] = icon
        return icon
    }
}

多次调用同一个图标时,可以节省内存。

有就取出,没有就创建并缓存
有就取出,没有就创建并缓存

那么,现在可以不用 init 了吗?

不,绝对不是。

工厂并不是取代 init,而是封装它的工具。

即使在工厂方法中,最终也要调用 init 来创建对象。

如果只是简单的值类型,或者创建逻辑很明确,直接使用 init 更好。

无谓地用工厂封装,只会让代码变长、变难读。

工厂真正发挥作用的场景另有其处。

  • 创建方式有多种,需要通过名称区分时
  • 需要通过缓存、对象池等方式复用时
  • 想把复杂的初始化流程隐藏在一个地方时
  • 想返回协议类型来隐藏实现时

如果不符合这些条件,使用 init 才是正确选择。

Swift 标准库也会这样区分使用。

Array(repeating:count:)是 init,但像UIColor.systemBlue这样的静态属性,实际上也是工厂的近亲。


实际工作中的判断标准总结

我把自己感到困惑时使用的标准整理成了表格。

情况 推荐
存储简单值 init
创建方式有多种 工厂方法
实例复用・缓存 工厂方法
隐藏实现并返回协议 工厂方法
初始化只需一行 init
感到困惑时,我总会想起这张表来做选择
感到困惑时,我总会想起这张表来做选择

总之,init 处理“如何创建”,而工厂处理“创建什么以及为什么创建”。

两者不是竞争关系,而是职责不同的搭档。

建议先从 init 开始,等创建逻辑变复杂后再迁移到工厂。

记住今天介绍的标准,代码就会整洁许多。祝你 Swift 编程愉快!

延伸阅读