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

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个回答

1426

摘自Brian Button的话:

  1. 它们通常被用作全局实例,为什么这样做不好?因为你将应用程序的依赖项隐藏在代码中,而不是通过接口公开它们。为了避免传递参数而将某些东西设为全局变量是一种坏味道

  2. 它们违反了单一责任原则:因为它们控制自己的创建和生命周期。

  3. 它们本质上导致代码紧密耦合。这使得在许多情况下很难对它们进行虚拟测试。

  4. 它们在应用程序的整个生命周期中携带状态。这会影响测试,因为你可能会出现需要有序执行测试的情况,而这对于单元测试来说是绝对不可取的。为什么?因为每个单元测试都应该独立于其他测试。


360
我不同意你的观点。由于评论只允许600个字符,我已经撰写了一篇博客文章来评论此事,请查看下面的链接。http://jorudolph.wordpress.com/2009/11/22/singleton-considerations/ - Johannes Rudolph
62
对于第1点和第4点,我认为单例模式很有用,事实上对于缓存数据(尤其是来自数据库的数据)几乎是完美的选择。在建模单元测试时,与之相关的复杂性远远被性能提升所超越。 - Dai Bok
60
使用代理模式来缓存数据(特别是来自数据库的数据)... @ Dai Bok - oz10
67
哇,回复很棒。可能是我用了过于激烈的措辞,请记住我是在回答一个带有负面倾向的问题。我的答案旨在列举由于糟糕的 Singleton 使用而出现的“反模式”清单。完全透明地说; 我也会时不时地使用 Singleton。在 Stack Overflow 上还有更中立的问题,可以作为讨论 Singleton 何时被认为是一个好主意的绝佳论坛。例如,https://dev59.com/gHVC5IYBdhLWcg3woSxW - Jim Burger
21
在多线程环境中,它们不太适合用作缓存。当多个线程争夺有限的资源时,很容易破坏缓存[由单例维护]。 - monksy
显示剩余21条评论

496

单例模式只解决一个问题。

资源争用。

如果您有一些资源,

(1) 只能有一个实例,并且

(2) 您需要管理该单个实例,

那么您需要一个单例

并不多见。日志文件是最常见的例子。您不想仅仅放弃一个单独的日志文件。您希望正确地刷新、同步和关闭它。这是一个必须被管理的单个共享资源的示例。

很少需要使用单例。它们之所以不好,是因为它们感觉像一个global,并且它们是GoF 设计模式书中的成员。

当您认为需要全局变量时,您可能正在犯一个可怕的设计错误。


53
硬件也是一个例子,对吧?嵌入式系统有很多可能使用单例的硬件,或者可能只有一个大的?<微笑> - Jeff
190
完全同意。有很多坚定的“这种做法是不好的”言论在流传,而没有承认这种做法可能有其适用之处。通常,这种做法之所以被认为“不好”,是因为经常被误用。只要适用得当,单例模式并没有本质上的问题。 - Damovisa
55
真正基于原则的单例非常罕见(打印队列肯定不是,日志文件也不是 - 请参考log4j)。通常情况下,硬件是因为巧合而非原则上的单例。连接到 PC 的所有硬件最多只是一个巧合性的单例(考虑多个显示器、鼠标、打印机、声卡)。即使是价值5亿美元的粒子探测器也只是因为预算限制和巧合而成为单例 - 这在软件中并不适用,因此不存在单例。在电信领域,电话号码和物理电话都不是单例(考虑 ISDN、呼叫中心)。 - digitalarbeiter
26
硬件可能只有一种资源的实例,但硬件不是软件。在开发板上,一个串口没有理由被建模为单例模式。事实上,这样建模只会让它更难移植到新的有两个串口的板子上! - dash-tom-bang
21
那么,您会编写两个相同的类,重复了许多代码,只是为了创建代表两个端口的两个单例?使用两个全局SerialPort对象如何?干脆不将硬件建模为类呢?而且,为什么要使用单例,这也意味着全局访问?您想让*代码中的每个函数都能访问串行端口吗? - jalf
显示剩余18条评论

386
一些编码狂热者认为单例模式只是一个被吹嘘的全局变量。就像许多人讨厌goto语句一样,有些人讨厌使用全局变量的想法。我见过几个开发人员不遗余力地避免使用全局变量,因为他们认为使用它就是失败的承认。奇怪但却是事实。
实际上,单例模式只是一种有用的编程技术,是您概念工具包中的一部分。有时候您会发现它是理想的解决方案,所以会使用它。但仅仅因为它是设计模式而使用它,这样做就和拒绝永远使用它一样愚蠢,因为它只是一个全局变量

17
我在 Singleton 模式中看到的问题是,有些人使用它们来代替全局变量,因为它们在某种程度上更好。但问题是(依我看来),在这些情况下,Singleton 带来的东西都是无关紧要的。(例如,在非 Singleton 中实现首次使用构造函数是微不足道的,即使它实际上没有使用构造函数来完成它。) - dash-tom-bang
23
“我们”看不起它们的原因是因为“我们”经常看到它们被错误地使用,而且太频繁了。“我们”知道它们确实有其存在的意义。 - Bjarke Freund-Hansen
5
@Phil,您说“有时候您可能会发现单例是最理想的解决方案,因此使用它”。好的,那么在哪种情况下我们会发现单例很有用? - Pacerier
27
@Pacerier,只要满足以下所有条件:(1)你只需要一个东西,(2)你需要将这个东西作为参数传递给大量的方法调用,(3)你愿意冒险在未来进行重构,以换取立即减少代码大小和复杂性的机会,就可以不必在各处传递该东西。 - antinome
22
我感觉这并没有回答问题,它只是说“有时候可能合适,有时候可能不合适”。好的,但是为什么?什么情况下会是这样呢?这个回答有什么不同于“中庸之道谬论”的地方吗? - Guildenstern
显示剩余4条评论

239
Misko Hevery, 来自 Google 的工程师,就这个主题撰写了一些有趣的文章...
「单例是病态说谎者」提供了一个单元测试示例,说明了单例可能会使依赖关系变得复杂,导致难以启动或测试应用程序。虽然这是一种极端滥用的情况,但他所表达的观点仍然有效:

单例只不过是全局状态,全局状态意味着你的对象可以秘密地获取其 API 中未声明的内容,因此,单例会让你的 API 变成病态说谎者。

「单例都去哪儿了?」阐述了依赖注入已经让获取实例变得容易,在构造函数中需要实例时,它可以减轻第一篇文章中抨击的糟糕全局单例背后的基本需求。

8
目前Misko关于这个主题的文章是最好的。 - Chris Mayer
26
第一个链接实际上并没有涉及单例模式的问题,而是假定类内部存在静态依赖关系。可以通过传递参数来修复所提供的示例,然后仍然使用单例模式。 - DGM
19
@DGM:确实 - 实际上,这篇文章的“理由”部分和“单例是原因”的部分存在巨大的逻辑矛盾。 - Harper Shelby
1
Misko的信用卡文章是滥用此模式的极端例子。 - Alex
2
阅读第二篇文章,单例实际上是对象模型的一部分。但是,与全局访问不同,它们是私有的,只能由工厂对象访问。 - FruitBreak
显示剩余4条评论

145

我认为混淆的原因在于人们不知道单例模式的真正应用。我再强调一遍,单例不是用于包装全局变量的模式。单例模式只应用于确保在运行时期间仅存在一个给定类的实例

人们认为单例是邪恶的,是因为他们在将其用作全局变量。正是由于这种混淆,才导致单例模式被轻视。请不要混淆单例和全局变量。如果按照它所设计的目的去使用,你会从单例模式中获得极大的好处。


25
整个应用程序都能使用的那个实例是什么?哦,是全局的。“Singleton不是将全局变量包装成模式”的说法要么是天真,要么是误导性的,因为按定义,该模式会将一个类包装在一个全局实例周围。 - cHao
28
当然,你可以在应用程序启动时创建一个类的实例,并通过接口将该实例注入到使用它的任何地方。实现不需要关心只能有一个实例存在。 - Scott Whitlock
5
@Dainius:最终实际上并没有这样的东西。当然,你不能随意用另一个实例替换该实例。(除非在一些让人扣脸的时刻你确实这么做了。我曾经看到过setInstance方法。)不过这几乎无关紧要——那些“需要”单例的蒟蒻们对封装一窍不通,也不知道可变全局状态有何问题,所以他为每个字段都提供了设置器(是的,这种情况经常发生。几乎我见过的所有单例都是设计上可变的,而且通常令人尴尬)。 - cHao
5
在很大程度上,我们已经有了这样的意识。“更青睐组合而非继承”这个概念已经存在一段时间了。当继承确实是最佳解决方案时,你当然可以自由地使用它。单例模式、全局变量、线程、goto等也是如此。在许多情况下,它们可能会起作用,但仅仅“有效”还不够——如果你想违背常规智慧,你最好能够证明你的方法比传统解决方案更好。我还没有看到过这种情况适用于单例模式的案例。 - cHao
4
为了避免我们相互之间的谈话产生偏差,我并不仅仅是在谈论全局可用的实例。这种情况有很多。当我提到“大写S的Singleton”时,我指的是GoF的Singleton模式,它将单个全局实例嵌入到类本身中,通过getInstance或类似的方法公开它,并防止存在第二个实例。实际上,在那个时候,你甚至可以完全没有这个一个实例。 - cHao
显示剩余6条评论

76

单例模式的一个比较不好的地方是很难进行扩展。如果想改变它们的行为,基本上必须内置某种装饰器模式或类似的东西。此外,如果有一天你想以多种方式完成某个任务,根据代码布局的方式,更改可能会相当痛苦。

需要注意的一点是,如果确实使用了单例模式,请尽量将它们传递给需要它们的人,而不是直接访问它们...否则,如果您决定以多种方式完成单例模式执行的任务,每个类都嵌入了对该单例的依赖关系,则更改将非常困难。

因此,简而言之:

public MyConstructor(Singleton singleton) {
    this.singleton = singleton;
}

与其说是:

public MyConstructor() {
    this.singleton = Singleton.getInstance();
}

我相信这种模式被称为依赖注入,通常被认为是一件好事。

但是,就像任何模式一样...需要仔细思考并考虑在给定情况下是否不适当地使用它...通常规则都是可以打破的,而模式也不应该毫无考虑地随意应用。


19
如果你在每个地方都这样做,那么你必须到处传递单例的引用,因此你不再拥有单例了。 :) (在我看来,这通常是一件好事。) - Bjarke Freund-Hansen
3
“胡说八道,你在说什么?Singleton 只是一个类的实例,只被实例化一次。引用这样的 Singleton 并不会复制实际对象,它只是引用它 - 你仍然拥有同样的对象(即:Singleton)。" - M. Mimpen
2
@M.Mimpen:不,一个大写的S Singleton(也就是这里所讨论的)是一个类的实例,它(a) 保证 只会存在一个实例,(b)并且通过类自身内置的全局访问点进行访问。如果您已经声明不允许调用getInstance(),那么(b)就不再完全成立。 - cHao
3
@cHao 我不是在跟你说话,或者你不明白我在评论谁——那个人是Bjarke Freund-Hansen。Bjarke声称一个单例有多个引用会导致有多个单例,这显然是不正确的,因为没有进行深拷贝。 - M. Mimpen
7
我理解他的评论更多是指语义效果。一旦禁止调用getInstance(),你实际上就丢掉了单例模式和普通引用之间唯一有用的区别。就代码的其余部分而言,单例已不再是一个属性。只有调用者需要知道或关心是否存在多个实例。对于只有一个调用者的情况,类可靠地强制执行单例模式的成本比让调用者存储一个引用并重用它更加费力和缺乏灵活性。 - cHao
显示剩余2条评论

71
单例模式本身并不是问题。问题在于,使用面向对象工具开发软件的人没有牢固掌握OO概念时,常常会使用该模式。当在此上下文中引入单例模式时,它们往往会变成包含每个小用途的帮助方法的难以管理的类。
从测试角度来看,单例模式也是一个问题。它们往往使得编写孤立单元测试变得困难。控制反转(IoC)和依赖注入是旨在以面向对象的方式克服这个问题的模式,适合进行单元测试。
垃圾收集环境中,单例模式也可能成为内存管理方面的问题。
还有多线程场景,其中单例模式可能成为瓶颈,也可能成为同步问题。

7
我知道这是一个多年前的帖子。嗨,@Kimoz,你说:单例在内存管理方面很容易成为问题。能否详细解释一下单例和垃圾回收方面可能出现的问题? - Thomas
@Kimoz,这个问题是在问“为什么单例模式本身不是一个问题?”,而你只是重复了这一点,却没有提供任何一个有效的单例模式使用案例。 - Pacerier
@Thomas,由于单例按定义只存在一个实例,因此通常很难将唯一引用赋值为null。虽然可以这样做,但这意味着您完全控制了单例在应用程序中不再使用的时间点。这种情况很少见,并且通常与单例爱好者所寻求的相反:一种简单的方法来始终访问单个实例。 在像Guice或Dagger这样的DI框架上,无法摆脱单例,它会永远留在内存中。(尽管容器提供的单例比自制的单例要好得多)。 - Snicolas

56

单例模式通过静态方法实现。但是由于静态方法无法被模拟或存根,因此进行单元测试时,人们通常会避免使用它们。在本网站上,大多数人都是单元测试的支持者。通常最广泛接受的避免使用静态方法的约定是使用控制反转模式。


11
听起来更像是单元测试的问题,可以测试对象(单元)、函数(单元)、整个库(单元),但在测试类中的静态内容时会失败 (也是单元)。 - v010dya
3
你不是应该模拟所有外部引用吗?如果是这样,那么 moc singleton 有什么问题呢?如果不是,那么你真的在进行单元测试吗? - Dainius
@Dainius:模拟实例比模拟类要容易得多。您可以想象从应用程序的其余部分中提取要测试的类,并使用虚假的“Singleton”类进行测试。但是,这会极大地复杂化测试过程。首先,现在您需要能够随意卸载类(在大多数语言中并不是真正的选项),或为每个测试启动新的VM(读取:测试可能需要花费成千上万倍的时间)。其次,对“Singleton”的依赖是一个实现细节,现在正在泄漏到您的所有测试中。 - cHao
Powermock可以模拟静态的内容。 - Snicolas
模拟对象是否意味着创建真实对象? 如果模拟不会创建真实对象,那么类是单例还是方法是静态的又有什么关系呢? - Arun Raaj
你可以在大多数流行的编程语言的模拟库中模拟静态类方法。但这并不意味着你应该在任何地方都使用静态方法 :) - Juha Untinen

47
单例在集群方面也存在问题。因为这样,你的应用程序中就不再有“一个确切的单例”了。
考虑以下情况:作为开发人员,您必须创建一个访问数据库的 Web 应用程序。为了确保并发数据库调用不会相互冲突,您创建了一个线程安全的 SingletonDao:
public class SingletonDao {
    // songleton's static variable and getInstance() method etc. omitted
    public void writeXYZ(...){
        synchronized(...){
            // some database writing operations...
        }
    }
}

你确信应用程序中只存在一个单例,并且所有数据库都通过这个唯一的“SingletonDao”进行。你的生产环境现在看起来像这样: Single Singleton 到目前为止,一切都很好。
现在,考虑你想在集群中设置多个Web应用程序实例。现在,你突然有了这样的情况:

Many singletons

这听起来很奇怪,但是现在你的应用程序中有很多单例。而这正是单例不应该存在的情况:它不能有多个对象。特别是在像这个例子中想要对数据库进行同步调用时,情况会变得非常糟糕。
当然,这是单例使用不当的一个例子。但是这个例子传达的信息是:你不能依赖于应用程序中恰好有一个单例实例 - 特别是当涉及到集群时。

4
如果你不知道如何实现单例模式,那么你就不应该这样做。如果你不知道自己在做什么,你应该先找到答案,只有在找到答案后才能开始做需要做的事情。 - Dainius
7
很有趣,我有一个问题。如果单例模式 - 每个都在不同的机器/JVM 上 - 连接到一个数据库,那么到底存在什么问题呢? 单例模式的作用域仅限于特定的 JVM,即使在集群中也是如此。与其仅仅从哲学上说这种情况很糟糕,因为我们的意图是跨应用程序使用单个对象,我宁愿看到由于这种安排可能会出现的任何技术问题。 - Saurabh Patil
2
你描述了一个可以通过数据库机制Transaction来解决的问题的解决方案。像Spring这样的框架默认使用Singletons作为bean的表示,大家都用了好几年了。你只是选择了错误的解决方案来解决你的问题。 - Mr Jedi
1
“确保并发数据库调用不会相互冲突” - 这不是数据库管理并发访问的任务吗?ACID?I=隔离性? - Thomas Weller

45
  1. 它很容易被误用为全局变量。
  2. 依赖单例的类相对来说更难在隔离环境下进行单元测试。

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