单例与单实例之争

450 查看

今天,在 DevMonologue 我们将要讨论一个设计概念,它已经困扰我有一段时间了。它不仅仅关系到 iOS 开发,在编程中也具有普遍意义。

我不会说我是软件架构的专家。但根据我的经验,我发现单例(singleton)与单实例( single instance)之争问题是许多项目都会遇到的,而很多人并没有意识到这一点。

这就是为什么我想分享一些想法,关于如何避免重大的设计缺陷的原因。正如我所说,我并不一定能保证这里写的内容在所有情况下都是 100% 正确的,所以欢迎大家慷慨反馈。我很欣慰看到这篇文章变成大家分享经验的讨论稿,或许还能在这领域给一些常见问题提供解决方法。所以,不要羞涩,在讨论区给我们写几行评论吧。

什么是“单例与单实例之争”的一派胡言?

废话有点多了,让我们言归正传。我所说的“单例与单实例之争”是什么意思呢?

“单例与单实例之争”的起源

现在,我不确定(在历史上)谁引进了这个词,但我可以告诉你,我第一次看到它是在哪。这是在一本叫《Working effectively with legacy code》,由 Michael Feathers 所著的书。如果你还不知道这本书,我建议你去查查。书中有大量实用的技巧,即使你觉得你的工作不接触遗留代码,你也会从那本书中获益良多,相信我。

定义

“单例与单实例之争”背后的原理非常简单。它只是指明了无论什么情况下,在平常使用单例对象时,你应该考虑只用一个单实例的情况。这是什么意思呢?

一个单例是一个严格遵守规则的类,该规则规定,该类只能有一个对象,并且更多的时候,该对象在整个程序都可以被访问。

另一方面,一个“单实例”,意味着该类本身不是单例,但是(在更高层次上),你要确保该类有唯一一个实例。

初看,这似乎并不重要,甚至单例更适合,因为它的用法更明确。让我们看看我是否能以其他方式说服你……

负面作用

破坏封装

在这两种情况中的类应只有一个对象。但一个单例积极严格遵守规则,而“单实例”却太单纯,它只是假设它的用户会知道不去创建多个对象。大家都知道,当在这样的场景下,尽可能直接明了会更好。所以就封装而言,单例获胜。

使用方便

开发者是懒惰的,而且(理所当然)喜欢简单的接口。并且就尽可能方便而言,你不能打败一个单例。你所需要做的只是导入单例类(如果必须的话),然后调用返回共享实例的方法。似乎没有比这更好的了。而“单实例”的使用,你将需要找出谁拥有该对象,以及你如何获得它。

然而,易于使用不总是件好事。我希望当你阅读最后一段时提高警觉。更多的内容在后面。

正面作用

在测试驱动开发中的“单例与单实例之争”

像许多你会在《Working effectively with legacy code》中找到的主题一样,这一点与测试驱动开发有关。但即使你不认同测试驱动开发,先不要关闭标签。虽然“单例与单实例之争”极大地简化了测试驱动开发,它也是所有项目中的强有力的设计决策。

在一般测试驱动开发中,非常重要的是每个测试需要隔离运行,运行环境需要在各个测试之间复位。这让单例成为问题。因为在应用的整个生命周期只有一个对象持续存在,你不能确保以前的测试数据不再遗留在环境中(还要注意,大多数的 IDE 可以并发运行单元测试,并且不能确保测试是顺序运行的)。有办法对付这个问题,但通常,当使用测试驱动开发,“单例与单实例之争”是 0:1。

限制访问

这就是上文讨论的“易于使用”的弊端。但,为什么我们需要尽量让它难以访问一个对象?

那么,通用访问有一个固有问题。如果一个应用的所有部分都可以访问一个对象,那么当有坏情况发生时它往往很难知道问题是什么。如果你追踪一个单例对象方法,而这方法有 30 个其他对象访问,这并不容易找到问题的来源。

另外一个问题是,你真的让它对使用你的类的人都易于访问的话,那么他们就会不断地使用它……这导致的正是我前面写的——每个人似乎都从单例类中调用方法。

现在,对于一般通用的功能,这些东西都是可以有的,应该不会造成问题。你不可能从不使用单例。但我觉得人们(包括我)都过度使用它们。我给你举个例子:

“单例与单实例”问题的一个例子

对我来说,单例设计模式是非常有用的,但不少开发者似乎有些滥用了。当你为你的类选择使用单例时,你最好在你决定之前认真想清楚它是否真的需要。它是不是绝对只拥有唯一的实例,如果有两个存在,它是否会破坏你的架构?或者它只是需要保证一个实例就足够了?

当人们只是需要一个全局变量,这是单例模式最常见的滥用情况。因为全局变量在现代编程标准中是备受谴责的,但它们仍是有用的,开发者们可能禁不住创建一个单例,这样该对象就可以在全局被访问。所以,允许我从前面的段落问一个问题:

绝对必要只拥有一个实例吗……或者只是要求一个实例就足够了?

不,这没有必要!

然而,这是“单例与单实例之争”选择的一个比较粗糙的例子。虽然这样使用可能不会让你陷入严重的麻烦,但你一定要远离这种情况。我们需要深入挖掘进入真正的问题。

想想我们可爱的 MVC 模式。尤其是控制器部分——我们的业务逻辑。你见过或参与过多少项目,它们在业务逻辑中使用大量单例的?坦诚面对它。CommunicationsManager、DataManager、NotificationsManager、LoginManager……它们都有可能是单例对象。但它们就应该是吗?

让我们再想想最后一个,LoginManager。它可能是一个管理用户会话的对象——也许包含了一个令牌(token)、cookie、用户凭据?

大多数应用程序只允许同时单个用户登录。所以,LoginManager 类真的需要有唯一的对象,对不对?并且单例模式很适合,不是吗?在这种情况下,允许我问一个问题:

绝对必要只拥有一个实例吗……或者只是要求一个实例就足够了?

是的!存在两个 LoginManagers 会出错!并且,在这里有些可疑的事情。这种使用情况会怎样:

  • 登录
  • 注销
  • 以其他身份重新登录

哦!LoginManager 似乎真的需要拥有唯一的实例,但该实例在应用整个生命周期中可能不是同一个。所以,我们应问的问题实际上是:

绝对需要拥有唯一,且在应用整个生命周期都不改变的实例吗?

那么,出现在大多数项目(在我的经历中)的是,LoginManager 实例(例如)只是在两次登录之间保持相同。为了有效运行,一旦用户注销,所有用户信息会被从实例中删除。这应是相当容易的。这能有多难?一个用户会话只需要通过几个条件确认——可能一个令牌或者一个用户名。但对于那些预缓存的用户朋友列表或头像或密码会怎样。这种代码似乎总在维护期间被破坏。你不能依赖于你的同事(甚至是你)总记住在注销时清除数据。这样做不合理。

而下一次,当你忘记清除该用户的令牌会发生什么?你最终甚至可能登录错误的用户!

现在,如果只有我们的 LoginManager 不是单例……我们只想当用户注销完成时删除对象。我们不会那么担心类在注销期间可能没能清除所有数据。

这故事很接近现实,在软件开发中没有什么是永远的。所以,不要太执着于你的实例对象。否则,它们会把你的小心脏打成碎片!

最后,我觉得这就是今天关于“单例与单实例之争”独白吧。感谢你的阅读!