单例模式有哪些缺点或不足之处?

2167

单例模式GoF设计模式书中的成员之一,但最近似乎被开发者世界所遗弃。我仍然经常使用很多单例模式,特别是对于工厂类,虽然你必须小心处理多线程问题(像任何类一样),但我无法理解为什么它们如此糟糕。

特别是Stack Overflow似乎认为每个人都认为单例模式是邪恶的。为什么呢?

请用“事实、参考资料或专业知识”来支持您的答案。


7
最近在尝试调整代码时,我必须说使用单例设计模式让我感到很沮丧。由于我只是在业余时间做这个,我几乎懒得重构它。这对生产力来说是个坏消息。 - Marcin
82
回答中有很多“缺点”,但我也希望看到一些好的例子来对比坏的例子,以展示这种模式何时是好的。 - DGM
51
我在几个月前写了一篇关于这个主题的博客文章:http://jalf.dk/blog/2010/03/singletons-solving-problems-you-didnt-know-you-never-had-since-1995/ --让我直言不讳地说。 我个人无法想出一个情况,其中 Singleton 是正确的解决方案。 这并不意味着不存在这样的情况,但是...说它们很少见是轻描淡写了。 - jalf
9
@AdamSmith 这并不意味着你必须这样做,但是它意味着你可以像那样访问它。如果你不打算像那样访问它,那么首先将它作为单例模式的意义就很小了。因此,你的论点实际上是“如果我们不将其视为单例模式,那么制作一个单例模式也没有什么坏处。是啊,太好了。如果我不开车,我的车也不会污染环境。但这样做可能比一开始就不买车更容易。;)(完全透明化:我实际上没有车) - jalf
55
这个话题最糟糕的部分是,那些讨厌单例模式的人很少给出具体建议来代替它。例如,这篇SO文章中的所有期刊文章和自我发布的博客链接都在详细阐述为什么不使用单例模式(它们都是非常好的理由),但它们在替代方案上极其含糊。很多只是泛泛而谈。我们试图教新程序员为什么不要使用单例模式,但没有太多好的第三方反例可供参考,只能用一些假想例子。这真是令人疲倦。 - Ti Strga
显示剩余19条评论
36个回答

4
当几个人(或团队)到达相似或相同的解决方案时,就会出现一种模式。很多人仍然使用单例模式的原始形式或使用工厂模板(Alexandrescu的现代C++设计中有很好的讨论)。并发和管理对象生命周期的难度是主要障碍,前者很容易像您建议的那样进行管理。
像所有选择一样,单例模式有其优缺点。我认为它们可以适度使用,特别是用于在应用程序生命周期内存活的对象。它们类似于(可能也是)全局变量,这可能引起了纯洁主义者的反感。

4

以下是作者的一些反驳观点:

如果将来需要使类不再是单例,您将陷入困境 完全不是这样 - 我曾经遇到过一个单个数据库连接单例的情况,我想将其转换为连接池。 请记住,每个单例都通过标准方法访问:

MyClass.instance

这类似于工厂方法的签名。我所做的就是更新instance方法以从池中返回下一个连接 - 不需要进行任何其他更改。如果我们没有使用单例,那将会更加困难。

单例只是花哨的全局变量 无法否认,但所有静态字段和方法也都是如此 - 任何从类而不是实例访问的内容本质上都是全局的,而我并没有看到对使用静态字段的反弹如此之大?

不是说单例很好,只是在这里反驳一些“传统智慧”的观点。


2
不,你没有理解重点。在第一点中,我仍然没有创建另一个实例的选项。我可以希望和祈祷,下一次调用getInstance()时,它会给我一个不同的实例,但我仍然没有办法只说“我有一个实例。现在我需要另一个实例来完成另一个任务”。在第二点上,许多东西只是花哨的全局变量,但单例模式将这与许多不必要的负担(1个实例限制、复杂且容易出错的同步问题)耦合在一起。 - jalf
1
@Ewan:可变的静态字段是有害的,因为许多原因与单例模式相同。如果你没有看到对它们的反对意见,那么你还没有努力寻找;static只是C#/Java中“全局”的拼写方式。至于静态函数,如果没有静态变量,它们就不太成问题了;全局性最大的问题是“远程操作”和它带来的隐藏依赖关系,而一个没有互调存储空间(没有实例,也没有可变的静态变量)的函数最终会受到很大限制,无法依赖于什么(以及它可以搞乱什么)。 - cHao
1
完全不是这样的 - 我曾经遇到过一个单一数据库连接单例的情况,我想将其转换为连接池。你仍然希望这个类是单例的。它从单一连接变成了单一连接池。如果您想将数据库服务器拆分为两个独立的部分,您会很困惑。现在,您的连接池需要知道连接到哪个服务器。如果一开始没有使用单例,那么在 DIC 中的唯一更改就是使用第二个服务器的详细信息创建第二个 DB 连接对象,并将其注入到所有移至该 DB 的对象中。 - Sven

3
这是我认为目前回答中缺失的内容:
如果您需要每个进程地址空间中此对象的一个实例(并且您尽可能确信此要求不会改变),则应将其设置为单例模式。
否则,它就不是单例模式。
这是一种非常奇怪的需求,用户几乎从不关心。进程和地址空间隔离只是实现细节。仅当用户想要使用kill或任务管理器停止您的应用程序时,它们才会对用户产生影响。
除了构建缓存系统外,很少有理由确定某些东西每个进程只应有一个实例。日志系统呢?也许最好将其设置为按线程或更细粒度,以便更自动地跟踪消息的来源。应用程序的主窗口呢?要视情况而定;也许您希望所有用户文档由同一进程管理,那么该进程中将有多个“主窗口”。

3

马克·拉德福德(Mark Radford)在《Overload Journal #57 – Oct 2003》中撰写的文章“单例模式-反模式!”详细阐述了为什么单例模式被认为是一种反模式。该文章还讨论了两种替代设计方法来取代单例模式。


弗兰克,链接似乎无法工作? - user18443
tdyen,抱歉回复晚了!对我来说链接正常工作(我刚试了一下用Opera和Firefox)... - Frank Grimm

2
它模糊了关注点的分离。
假设您有一个单例,您可以从类中的任何位置调用此实例。您的类将不再像应该那样纯净。您的类现在将不再仅仅操作其成员和显式接收到的成员。这将会导致混乱,因为类的用户不知道类需要的足够信息是什么。封装的整个思想就是隐藏方法的实现细节,但如果在方法内部使用单例,则必须了解单例的状态才能正确使用该方法。这是反面向对象编程的。

5
不太确定这个答案是否正确。实际上,任何对象都可能存在类似的问题。真正的问题在于单例可以从任何地方访问。一个清晰的API和充足的文档可以防止客户端错误使用它。 - Tim Frey

1

我可以举几个例子:

  1. 它们强制实现紧耦合。如果您的单例存在于与其用户不同的程序集中,那么使用该程序集的程序将无法在不包含单例的程序集的情况下运行。
  2. 它们允许循环依赖关系,例如,程序集A可以具有对程序集B的单例的依赖项,并且程序集B可以使用程序集A的单例。所有这些都不会破坏编译器。

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