单例 vs 单一实例

单例 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 单一实例”的讨论就到这里,感谢阅读!

你可能感兴趣的:(单例 vs 单一实例)