单例模式:好的设计还是一个替代品?

69

单例模式是一个备受争议的设计模式,我对 Stack Overflow 社区对它们的看法很感兴趣。

请提供您意见的原因,而不仅仅是“单例模式适用于懒惰的程序员!”

这里有一篇非常好的文章讨论这个问题,尽管它反对使用单例模式: scientificninja.com: performant-singletons.

还有其他支持单例模式的好文章吗?

23个回答

56

支持单例模式:

  • 与全局变量相比,它们并不那么糟糕,因为全局变量没有标准强制的初始化顺序,你可能会因为简单或意外的依赖关系而看到非确定性错误。假设单例(假设它们在堆上分配)是在所有全局变量之后创建的,并且在代码中一个非常可预测的位置。
  • 对于资源延迟和缓存系统非常有用,例如慢速I/O设备的接口。如果您聪明地构建了一个单例接口来访问慢速设备,并且没有人从任何地方调用它,您就不会浪费任何时间。如果另一段代码从多个位置调用它,则您的单例可以同时优化缓存,避免任何双重查找。您还可以轻松避免由单例控制的资源产生死锁。

反对单例模式:

  • 在C++中,没有优雅的方法自动清理单例模式。虽然有一些解决方法和略微hacky的方法,但没有简单、通用的方法可以确保您的单例的析构函数总是被调用。这方面的内存问题并不是很严重,只需将其视为用于此目的的全局变量更多即可。但如果您的单例分配其他资源(例如锁定某些文件)并且不释放它们,则可能会出现问题。

我的个人观点:

我使用单例模式,但在有合理替代方案的情况下避免使用它们。到目前为止,这对我工作得很好,并且我发现它们是可以进行测试的,尽管测试需要做更多的工作。


5
第一点中的假设有些牵强,通常在C++中单例并不是通过堆分配来创建的。 - Joris Timmermans
3
我对此并不确定,MadKeithV。通常情况下,一个成员函数(比如 MyClass* MyClass::get_instance())会检查一个静态成员指针,如果它为 NULL,则调用 new 在堆上分配共享实例。那么一个单例模式怎么可能存在于其他地方呢? - Tyler
关于C++的缺点:看看Meyers单例模式。 - Paul Manta
2
@Tyler:我刚刚发现以下代码支持@MadKeithV的不使用堆(heap)单例模式的观点。 class MySingleton { public: static MySingleton &getInstance() { static MySingleton instance; return instance; } private: MySingleton(); ~MySingleton(); }; 参考 - https://dev59.com/qU3Sa4cB1Zd3GeqPrgjn - Forever Learner
问题标签:“语言无关”。答案中的第一点:“全局变量没有标准强制初始化顺序,...单例(假设它们分配在堆上)是在所有全局变量之后创建的”。这些显然不是语言无关的评论。 - Mark Amery

38

谷歌为Java开发了一个Singleton Detector,我认为它最初是谷歌所有代码的必备工具。摒弃单例模式的原因如下:

因为它们可能会给测试带来困难并隐藏设计中的问题。

有关更明确的解释,请参见谷歌的 '为什么单例模式有争议'。


我怀疑说这个“必须在谷歌生产的所有代码上运行”有些牵强。(首先,他们可能使用其他编程语言。) - Tyler
我相信Java、Python和C++是Google认可的编程语言。 - Brian
1
他们也可以使用JavaScript:http://steve-yegge.blogspot.com/2007/06/rhino-on-rails.html - CTT
2
谷歌也有一个会计部门,这是否意味着每个开发者都应该有一个?禁止某些结构的决定是谷歌的商业决策,更多地反映了他们的内部环境。毫无疑问,一些手工调整的机器代码仍在那里被调整,但出于商业原因,并不是任何人都可以这样做。 - Dustman
1
去读一些谷歌的代码,他们并不遵循这个规则。 - jkp

31

单例只是穿上花哨衣服的全局变量。

全局变量有其用处,单例也有其用途,但如果你认为使用单例而不是使用恶心的全局变量(每个人都知道全局变量很糟糕),你就会被误导了。


1
全局变量和方法。甚至一个全局对象... - dqhendricks
1
一个单例不一定是真正的全局对象。 - iheanyi

27

Singleton的目的是确保一个类只有一个实例,并提供对它的全局访问点。大多数情况下,重点关注单个实例点。想象一下,如果它被称为Globalton,它将听起来不那么吸引人,因为这强调了(通常)全局变量的负面含义。

对单例模式进行反驳的大部分好的论点都与测试中遇到的困难有关, 因为要为它们创建测试替身并不容易。


11
为什么一个类应该确保它只有一个实例?这几乎会破坏面向对象编程的目的。 - Patrick Glandien
3
通常情况下,您只需要一个日志记录工具?或者您正在包装某个神秘的dll,并且不希望让它能够被加载两次? - Calyth
2
@Calyth 但是为什么要把这个责任放在类本身呢,明显是系统的责任。(任何一个对象实例都可以正常工作,但如果不能满足每次只能同时存在一个对象的约束条件,则系统可能会失败) - Rune FS
@RuneFS 因为我只需要在一个类中编写代码来强制实现单例。如果系统强制执行,我就必须在系统的各个地方编写代码来强制实现单例。检测一个类是否符合特定行为比检测100个不同类是否都反映了该行为要容易得多。 - iheanyi
@iheanyi,你没有抓住重点。你把责任放在“100个类”上,这不是一种使系统负责的设计。我主张将责任放置在系统负责实例化的任何部分(如IoC或类似)中。 - Rune FS
@RuneFS 啊,谢谢。我一直在阅读有关此事的更多信息,我认为我已经得出结论,即单例的唯一受益用途是作为编译时断言,即我只创建了一个特定对象。有时我可能会在设计/实现阶段早期使用它作为健全性检查,但是一旦事情变得清晰明确,就会删除“单例”保护。我现在同意您的评论-我得出结论,为了确保不隐藏依赖项,您需要将单例对象的引用传递给用户的构造函数。如果您这样做,实际上您并不需要单例。 - iheanyi

23

1
哇,真的很有趣的东西,但我想要解决方案! :-) 交易 - Sander Versluys
4
我认为其中一个“解决方案”是依赖注入,它可以自动处理实例的注入,并且可以使单例不再成为反模式。 :) 请查看Guice!http://code.google.com/p/google-guice/ - Bleadof
  1. 第一篇文章存在问题。你可以将单例作为构造函数参数,完全解决文章中的问题。
  2. 单例不一定是全局的。因此,在文章中用“共享对象”替换“单例”,一切仍然有效。
  3. 只有在第三篇文章中,他才会推翻前两篇文章中所声称的所有废话,并说“哦,我所指的坏事是设计模式中真正全局访问‘单例’的情况。” 嗯,那当然。所以你写了三篇文章,只是想说,全局变量很糟糕,特别是在复杂的情况下使用。
- iheanyi

20

单例模式并不是一种可怕的模式,但是它经常被误用。我认为这种误用是因为它是比较简单的模式,而且大多数新手都很容易被单例模式所吸引。

Erich Gamma曾经说过,单例模式是他希望GOF书籍中没有包含的一个糟糕设计。但我对此持有不同意见。

如果该模式是用来在任何给定时间创建一个对象的单个实例,那么该模式就被正确地使用了。如果单例模式被用于提供全局效果,那么就被错误地使用了。

缺点:

  • 你会在调用单例模式的代码中与一个类进行耦合
    • 这会给单元测试带来麻烦,因为很难用模拟对象替换实例
    • 如果以后需要重构代码,因为需要多个实例,那么与将单例类传递给使用它的对象(使用接口)相比,这个过程更加痛苦

优点:

  • 在任何给定时间只有一个类的实例存在。
    • 通过设计,你正在强制执行这一点
  • 在需要时创建实例
  • 全局访问是一个副作用

4
“全球访问是一个副作用”我很高兴有人指出这一点。全球方面可能是单例最被误用的部分。 - RobS
我敢打赌Eric Gamma对单例模式的看法来自于许多辛苦积累的经验。 - gtrak
1
我认为这个答案应该排在最前面。 - Maxime Pacary
@RobS 我不明白。你如何在不访问其全局实例的情况下使用单例并保持你所链接的优点呢?全局访问不是副作用。 - Shoe
@Jeffrey 这一点最好称为“全局引用”。原回答已经清楚地指出:“如果以后需要重构代码,因为需要多个实例,则比将单例类传递给对象更加痛苦。” - RobS

18

小鸡们喜欢我,因为我很少使用单例模式,而当我使用它时,通常是一些不寻常的东西。不开玩笑,我真的很喜欢单例模式。你知道为什么吗?因为:

  1. 我很懒。
  2. 没有任何问题会出现。

当然,“专家们”会围绕“单元测试”和“依赖注入”进行一系列谈论,但这只是一堆无用的废话。你说单例模式很难进行单元测试?没问题!只需将所有内容声明为公共的,将类转换成全局功能丰富的娱乐场所即可。你还记得1990年代的电视节目《高地人》吗?单例模式有点像它,因为:A. 它永远不会死亡;B. 只能有一个。所以不要再听那些DI乌龟的话,毫不犹豫地实现你的单例模式。下面是更多好的理由...

  • 每个人都在做。
  • 单例模式让你无敌。
  • 单例和“win”(或根据你的口音,“fun”)押韵。

8
我认为对于单例模式的使用存在着一个巨大的误解。这里的大部分评论都将其作为访问全局数据的地方。我们需要小心 - 单例作为一种模式,不是用于访问全局变量。
应该使用单例模式来确保给定类的只有一个实例Pattern Repository 上有关于单例模式的详细信息。

6

我曾经共事过的一位同事非常注重单例模式。每当出现类似于经理或老板那样的对象时,他会将其转换为单例模式,因为他认为应该只有一个老板。但是每次系统接受新需求时,都会发现有允许多个实例存在的充分理由。

我认为,如果领域模型规定(而不是“建议”)只能有一个实例,则应使用单例模式。其他情况下,只是类的偶然单一实例。


5

我一直在努力想出一种方法来帮助解决这个单例模式的问题,但我必须承认这很困难。我见过很少合法使用它们的情况,而且随着当前对于依赖注入和单元测试的推崇,它们变得更加难以使用。它们绝对是编程设计模式“仿效神仙”的体现,我曾经与许多从未读过“GoF”书籍但了解“Singleton”的程序员共事,因此他们了解“模式”。

然而,我不得不反对Orion的观点,大多数时候我看到的单例模式的滥用并不是全局变量穿上裙子,而更像是全局服务(方法)穿上裙子。有趣的是,如果您尝试通过CLR接口在SQL Server 2005的安全模式下使用单例模式,则系统将标记代码。问题在于您拥有超出任何给定事务之外的持久数据,当然,如果您将实例变量设置为只读,则可以解决该问题。

那个问题给我带来了大量的重做工作。


2
我从未理解为什么人们如此热衷于这种模式。我想可能是因为它“允许”您使用全局变量,这样可以节省重新思考设计的麻烦。但实际上,它并没有起到这个作用。 - Chris Huang-Leaver

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接