软件设计

Swift Monostate 模式:能成为 Singleton 的替代方案吗?

写到这里,曾怀疑 Singleton 真的可以一直这样用的人

4 分钟阅读
Swift Monostate 模式:能成为 Singleton 的替代方案吗? 封面图

写到这里,曾怀疑 Singleton 真的可以一直这样用的人

做 iOS 开发时,应该没有人从没用过 Singleton。我也一样。SomeManager.shared 这种代码在每个项目里至少都会出现好几处。

但从某个时候开始,这个.shared逐渐变得让人不舒服。测试时状态会互相干扰,团队成员也会从各处随意访问它。

后来我了解到 Monostate 模式。今天想分享我在 Swift 中使用 Monostate 的亲身体验,以及它是否真的能成为 Singleton 的替代方案。

先说结论:Monostate 是一种让状态保持共享、同时允许创建多个实例的模式。它和 Singleton 的目标相同,但实现方式完全相反。不过它不是万能方案,还是要根据具体情况选择。下面逐一展开。

Monostate 模式到底是什么?

Singleton 的核心是“只创建一个实例”。通过限制初始化器,只使用shared

Monostate 正好相反。实例可以创建任意多个,但其中的状态(数据)全部通过 static 共享。

也就是说,无论创建对象 A 还是 B,它们看到的数据都来自同一个地方。表面上像普通对象,内部却连接着同一份状态。

用 Swift 简单表示,大概是下面这样。

struct Settings {
    private static var _volume = 50   // 共享 static状态
    var volume: Int {                 // 像实例一样使用
        get { Settings._volume }
        set { Settings._volume = newValue }
    }
}
// 实例不同,但状态只有一份
var a = Settings(); a.volume = 80
print(Settings().volume)  // 80

即使新建Settings(),volume 仍然是 80。实例有多个,但状态只有一份,这就是 Monostate 的全部。

实例有三个,但它们看到的状态只有一个
实例有三个,但它们看到的状态只有一个

它和 Singleton 有什么不同?

最大的区别在于使用方的代码。

使用 Singleton 时,调用方必须始终意识到.shared,并想着“这是 Singleton”。使用 Monostate 时,只需像使用普通对象一样创建Settings(),不需要了解内部实现。

整理成表格如下。

分类 Singleton Monostate
实例数量 只有 1 个 可以有多个
共享内容 实例本身 状态(static 数据)
使用方 显式指定.shared 像普通对象一样
继承 麻烦 相对自由

尤其是继承很有意思。Singleton 很难继承,而 Monostate 是普通类型,因此扩展起来更加灵活。


那么,它能成为 Singleton 的替代方案吗?

说实话,有时可以,有时不行。

先说优点。使用方的代码更简洁,.shared这个全局访问点也不会散落在代码各处。由于接口和普通对象完全一样,之后调整结构时负担也更小。

Monostate 是一种“隐藏全局状态”的模式。看起来很方便,但隐藏的全局状态本身就是一把双刃剑。

问题就在这里。它表面上像普通对象,团队成员可能以为“这只是一个局部对象”而创建它,结果却发现状态其实是全局共享的,反而更容易混乱。

而且由于状态通过 static 共享,在多线程环境中仍然要同样注意并发问题。这一点和 Singleton 没有区别。

根据我的经验,在现代 Swift 中,我建议优先考虑依赖注入,而不是 Monostate。这样更容易测试,依赖关系也更清晰。

如果是现在的 Swift 项目,我会先选择 DI
如果是现在的 Swift 项目,我会先选择 DI

那么,什么时候适合使用呢?

总结来说,适合以下情况。

  1. 现有 Singleton 代码太多,想逐步移除时
  2. 想让使用方接口保持像普通对象一样简洁时
  3. 需要处理必须支持继承或扩展的全局状态时

反过来,如果是从零开始编写新项目,我会建议先考虑依赖注入或显式状态管理,而不是 Monostate。

我把各种模式放进工具箱,在适合的情况下再拿出来使用
我把各种模式放进工具箱,在适合的情况下再拿出来使用

归根结底,Monostate 不是“Singleton 的完美上位替代”,而是“性格不同的另一个选择”。把它放进工具箱,在适合的时候拿出来使用即可。

希望今天的内容能给那些厌倦.shared滥用的人提供一点小提示。与其盲目切换,不如认真想想什么最适合我们的项目。

延伸阅读