单例 vs 单一实例
原文:Singleton vs. single instance
欢迎大家来到 Monologue,今天我们讨论一个同程序设计相关的话题,其不仅适用于 iOS ,更适用于所有的程序开发。
虽然我并非是是程序设计方面的专家,但在个人看来,许多 app 项目对单例 / 单一实例的使用存在混淆不清的地方,更可怕的是,开发者们似乎还没有意识到。
因此,我想在这里同大家分享如何避免这样的设计缺陷。如前所述,我并非程序设计方面的专家,所以无法保证在这篇文章中所陈述的观点100%正确,欢迎大家批评指正。我更倾向于让将篇文章起到抛砖引玉的作用,引发讨论,从而解决当前的问题。所以,欢迎评论。
什么是“单例 vs 单一实例”
闲话少说,直入正题:什么是“单例 vs 单一实例”
追根溯源
我已记不得谁是第一个如此描述这个问题的人,但是我依然记得是从哪里读到这个术语的。那是一本名叫《玩转老旧代码》的书(Working effectively with legacy code by Michael Feathers)。如果你不知道这它,建议去读读。其中包含了许多有用的技巧,即使你认为自己从不需要和老旧代码打交道,依然可以从其中受益良多。
定义
“单例 vs 单一实例”表示一个很简单的概念:当你使用单例的时候,先问问自己是否可以改用单一实例。
按照单例模式设计的类(以下简称单例类)在整个 app 中有且只有一个实例对象,通常,在程序的任意地方都可以访问它。
相反,“单一实例”意味着类本身并非按照单例模式进行设计,但在使用时,我们每次只使用一个实例对象。
乍看上去,好像没什么大不了的,我们甚至会觉得单例更棒更好用。其实不然,且听我慢慢道来:
假象
封装性
不管是单例还是单一实例,我们都只使用一个实例对象。但是前者通过设计模式贯彻这一原则,后者仅仅依靠使用者主观遵守这个原则。很明显,在这种情况下,最好能够对使用者进行强制约束,所以,就封装性来看,单例胜出。
易用性
开发者都是懒人(也应该是),喜欢简单的接口。从这点上来讲,单例无人能及。只需要引入头文件(Swift 不用),调用返回单例对象的类方法,就 OK 了。够简单吧?如果使用单一实例
,我们首先必须搞清楚谁拥有这个实例对象 & 如何能够获取到它。
不过,好用并不总是好事。说到这里,希望大家能够有所警觉,我们继续往下看。
进阶
测试驱动开发
同许多《玩转老旧代码》探讨的主题一样,这个主题也提到了测试驱动开发
(以下简称 TDD)。即使你反对测试驱动开发,也不着急关闭页面。
TDD 的关键在于各项测试独立进行,程序环境在每次测试之间都会重置。此时,单例
会造成麻烦:整个 app 的生命周期中有且仅有一个实例对象存在,我们无法保证这个对象是否还残留有前一个测试的状态信息(另外还需注意,许多 IDE 可以同时运行多个单元测试,它们之间的顺序无法保证)。这个问题是可以解决的,但总的来说,针对 TDD,“单例 vs 单一实例”之间较量为 0:1。
限制访问
同“易用性”相反,有时,我们必须限制对于某些对象的访问。
嗯,全局访问
有着天生的缺陷。如果在整个程序的任何地方都可以访问一个对象,那么一旦出现问题,就很难知道是谁进行了误操作。试想一下找出一个被30个不同对象访问的单例对象除了问题,这种 debug 极为麻烦。
另一个问题就是越好用的东西就越会被频繁使用
。这就会造成上一段文字讨论的局面,所有对象都肆意的调用单例。
并不是说绝对不可以使用单例,从功能上来说这种方式没问题。但是它很容易被滥用(事实也是如此),例如下面的例子:
例子
就我个人而言,单例模式及其好用,但必须意识到我们正在滥用它。如果你选择使用它,请三思:是不是只能有一个实例对象;如果有两个同时并存,就会破坏程序的结构?换句话来说,是不是一个对象就够了?
最常见的滥用单例的典型:当我们需要一个全局变量时。许多现代编程语言都强调尽量避免使用全局变量
,但在有时我们不得不用。我们创建一个单例对象,因为在哪里都可以引用它(同全局变量),试着回答上面的问题:
是不是只能有一个实例对象,或者说是不是一个对象就足够了?
当然不是!
当然,上面只是举了一个很基础的例子。想要触及真正的问题, 就必须进一步深入挖掘。
以 MVC 架构中的控制器为例,它是处理业务逻辑的地方。我见过许多项目都在它们的业务逻辑中频繁使用单例:CommunicatinManager,DataManager,NotificationMananger,LoginMananger等等。它们都不约而同的使用了单例,但问题是:有必要吗?
拿 LoginMananger 来说,这个对象负责管理用户登陆周期,其包含 token / cookie / credential 等信息。
大多数 app 一次只允许一个用户登陆。所以,我们只一次只需要一个 LoginManager 实例对象。乍一看来,单例完美无缺。但回到上面的问题:
是不是只能有一个实例对象,或者说是不是一个对象就足够了?
是的,没错。如果同时出现两个 LoginManager 那就有问题了。等等,不觉得有点奇怪吗?考虑下面的情况:
- 登入
- 登出
- 再次登陆,但使用不同的证书
啊!虽然整个程序只需要一个 LoginMananger 实例对象,但是这个其在程序运行的期间发生了变化。所以,上述问题可以修正为:
整个 app 的生命周期中,是不是只能有一个
一成不变
的实例对象?
对于单例模式来说, LoginManager 对象在不同用户登陆周期之间持续存在。因此,用户登出时,这个对象必须清除其所保存的用户信息。貌似简单,用户的登陆状态是通过若干项信息表示的-token,用户名等。可以其他用户相关数据,诸如缓存的好友列表,头像,密码呢?这些信息是很难维护的。你不能指望你的同事(甚至你自己)记得在登出时清除数据。
某次,如果你忘记清理用户 token,会发生什么?用户可能会以错误的身份能登陆!
如果 LoginMananger 对象不是一个单例,我们只需在用户登出时删除这个对象即可。完全不用担心自己忘记清理数据。
同现实生活类似,在软件中没有什么是永恒的。所以别舍不得释放你的对象,否则难过的是你😢。
好吧,今天的关于“单例 vs 单一实例”的讨论就到这里,感谢阅读🙏!