单例模式的缺点是什么?

单例模式GoF模式书的全额付费成员,但它最近似乎被开发人员世界孤立了。我仍然使用相当多的单例,尤其是对于工厂类,虽然你必须对多线程问题(实际上就像任何类一样)有点小心,我不明白为什么它们如此糟糕。

Stack Overflow似乎特别假设每个人都同意Singleton是邪恶的。为什么?

请用“事实,参考资料或特定的专业知识”来支持你的回答

605994 次浏览

因为它们基本上是面向对象的全局变量,所以您通常可以以这样一种方式设计您的类,以便您不需要它们。

  1. 它很容易(ab)用作全局变量。
  2. 依赖于单例的类相对难以单独进行单元测试。

参见维基百科Singleton_pattern

它也被一些人认为是一种反模式,他们认为它被过度使用,在实际不需要类的唯一实例的情况下引入了不必要的限制[1][2][3][4]

参考文献(仅来自文章的相关参考文献)

  1. 亚历克斯·米勒我讨厌的模式#1:单例,2007年7月
  2. ^Scott Densmore.为什么单身是邪恶的,2004年5月
  3. ^Steve Yegge.单身人士被认为是愚蠢的,2004年9月
  4. ^J. B. Rainsberger,IBM明智地使用你的单身,2001年7月

模式本身没有什么问题,假设它被用于模型的某些方面,这是真正的单一。

我相信这种反弹是由于它的过度使用,反过来,这是因为它是最容易理解和实现的模式。

来自Google的Misko Heach有一些关于这个主题的有趣文章…

单身人士是病态的骗子有一个单元测试示例,说明了单例如何使弄清楚依赖链并启动或测试应用程序变得困难。这是一个相当极端的滥用示例,但他提出的观点仍然有效:

单例只不过是全局状态。全局状态使您的对象可以秘密获取其API中未声明的内容,因此,单例使您的API成为病态骗子。

辛格尔顿人都去哪儿了指出,依赖注入使得将实例获取到需要它们的构造函数变得容易,这减轻了第一篇文章中谴责的糟糕的全局Singletons背后的潜在需求。

它模糊了关注点分离

假设你有一个单例,你可以从类内的任何地方调用这个实例。你的类不再像它应该的那样纯。你的类现在将不再对其成员和它显式接收的成员进行操作。这会造成混乱,因为类的用户不知道类需要的足够信息是什么。封装的整个思想是向用户隐藏方法的方式,但如果在方法内部使用了单例,人们必须知道单例的状态才能正确使用该方法。这是反OOP

使用静态方法实现单例。进行单元测试的人避免使用静态方法,因为它们不能被嘲笑或存根。本网站上的大多数人都是单元测试的大力支持者。通常最被接受的避免它们的惯例是使用控制反转模式。

一些编码势利小人将他们视为只是一个美化的全球。就像许多人讨厌goto语句一样,还有一些人讨厌使用全球的想法。我见过一些开发人员不遗余力地避免使用全球,因为他们认为使用全球是承认失败。奇怪但确实如此。

在实践中,Singleton模式只是一种编程技术,它是你的概念工具包中有用的一部分。有时你可能会发现它是理想的解决方案,所以使用它。但是,仅仅为了吹嘘使用设计模式而使用它,就像拒绝使用它一样愚蠢,因为它只是全球

订阅关于Brian Button的评论:

  1. 它们通常被用作全局实例,为什么这么糟糕?因为你在代码中隐藏了应用程序的依赖关系,而不是通过接口暴露它们。制作全局的东西以避免传递它是一种代码气味

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

  3. 它们本质上导致代码紧密耦合这使得在测试中伪造它们在许多情况下相当困难。

  4. 它们在应用程序的生命周期中携带状态。测试的另一个打击是因为你最终会遇到需要订购测试的情况,这对单元测试来说是一个很大的不。为什么?因为每个单元测试都应该独立于另一个。

当几个人(或团队)得到相似或相同的解决方案时,就会出现一种模式。许多人仍然以原始形式使用单例或使用工厂模板(Alexandresu的现代C++设计中有很好的讨论)。并发和难以管理对象的生命周期是主要障碍,前者很容易按照你的建议管理。

像所有的选择一样,Singleton也有它的起起落落。我认为它们可以适度使用,特别是对于那些在应用程序生命周期中幸存下来的对象。它们类似于(并且可能是)全局变量的事实大概引发了纯粹主义者。

关于单例的一个相当糟糕的事情是你不能很容易地扩展它们。如果你想改变它们的行为,你基本上必须构建某种装饰器图案或类似的东西。此外,如果有一天你想有多种方法来做一件事,改变可能会相当痛苦,这取决于你如何布局代码。

需要注意的一点是,如果你确实使用单例,请尝试将它们传递给需要它们的人,而不是让他们直接访问它……否则,如果你选择有多种方式来做单例所做的事情,那么改变将是相当困难的,因为每个类都嵌入了一个依赖项,如果它直接访问单例。

所以基本上:

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

而不是:

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

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

就像任何模式一样……想一想,考虑它在给定情况下的使用是否不合适……规则通常是用来打破的,模式不应该不假思索地应用。

当您使用单例(例如记录器或数据库连接)编写代码时,然后您发现您需要多个日志或多个数据库,您就遇到了麻烦。

单例使得从它们移动到常规对象非常困难。

此外,编写非线程安全的单例太容易了。

你应该将所有需要的实用程序对象从函数传递到函数,而不是使用单例。如果你将所有它们包装成一个辅助对象,就可以简化这一点,如下所示:

void some_class::some_function(parameters, service_provider& srv){srv.get<error_logger>().log("Hi there!");this->another_function(some_other_parameters, srv);}

Mark Radford的辛格尔顿-反模式!(Overload Journal#57-Oct 2003)很好地解释了为什么Singleton被认为是一种反模式。本文还讨论了替代Singleton的两种替代设计方法。

从我的头顶:

  1. 如果您的单例驻留在与其用户不同的程序集中,则使用程序集在没有包含单例的程序集的情况下永远无法运行。
  2. 它们允许循环依赖,例如,程序集A可以有一个依赖于程序集B的单例,而程序集B可以使用程序集A的单例。所有这些都不会破坏编译器。

单例模式本身不是问题。问题在于,这种模式经常被使用面向对象工具开发软件的人使用,而他们对OO概念没有很好的掌握。当在这种情况下引入单例时,它们往往会发展成难以管理的类,其中包含每一个小用途的辅助方法。

从测试的角度来看,单例也是一个问题。它们倾向于使孤立的单元测试难以编写。控制反转(IoC)和依赖注入是旨在以面向对象的方式克服这个问题的模式,适合单元测试。

垃圾收集环境中,单例可能很快成为内存管理方面的问题。

还有多线程场景,其中单例可能成为瓶颈以及同步问题。

我不打算评论善与恶的争论,但自从Spring出现以来我就没有使用过它们。使用依赖注入几乎消除了我对单例、服务定位器和工厂的要求。我发现这是一个更高效、更干净的环境,至少对于我所做的工作类型(基于Java的Web应用程序)来说是这样。

太多的人将非线程安全的对象放在单例模式中。我见过DataContext(LINQ到SQL)在单例模式中完成的示例,尽管DataContext不是线程安全的并且纯粹是一个工作单元对象。

单例是一种模式,可以像任何其他工具一样被使用或滥用。

单例的坏处通常是用户(或者我应该说不恰当地使用单例来做它不打算做的事情)。最大的罪犯是使用单例作为虚假的全局变量。

我认为造成混乱的原因是人们不知道Singleton模式的实际应用。我不能强调这一点。Singleton是没有包装全局的模式。Singleton模式应该只用于保证给定类的一个且仅有一个实例在运行时存在。

人们认为Singleton是邪恶的,因为他们将其用于全局。正是由于这种混淆,Singleton被瞧不起。请不要混淆Singleton和全局。如果用于它的目的,您将从Singleton模式中获得极大的好处。

单例解决一个(且仅一个)问题。

资源争夺。

如果你有一些资源

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

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

你需要一个单例

示例不多。日志文件是最大的一个。您不希望只是放弃单个日志文件。您希望正确刷新、同步和关闭它。这是必须管理的单个共享资源的示例。

你很少需要单身人士。他们不好的原因是他们觉得自己是0号,他们是GoF设计模式书的全额付费成员。

当你认为你需要一个全局的时候,你可能犯了一个可怕的设计错误。

最近关于这个主题的文章由Chris Reath在无注释编码

注意:无注释编码不再有效。但是,链接到的文章已被另一个用户克隆。

链接

作者的一些观点:

如果未来需要让班级不单身,你被卡住了一点也不——我在这种情况下使用了一个数据库连接单例,我想把它变成一个连接池。请记住,每个单例都是通过标准方法访问的:

MyClass.instance

这类似于工厂方法的签名。我所做的只是更新实例方法以从池中返回下一个连接-不需要其他更改。如果我们没有使用单例,这会困难得多。

单例只是花哨的全局变量不能与之争论,但所有静态字段和方法都是如此-从类而不是实例访问的任何东西本质上都是全局的,我没有看到使用静态字段有这么多阻力?

并不是说Singletons是好的,只是在这里反驳了一些“传统智慧”。

单例的问题是范围增加的问题,因此耦合。不可否认,在某些情况下,您确实需要访问单个实例,并且可以通过其他方式完成。

我现在更喜欢围绕控制反转(IoC)容器进行设计,并允许容器控制生命周期。这给了你依赖于实例的类的好处,使你不知道存在单个实例的事实。单例的生命周期可以在将来更改。我最近遇到的一个这样的例子很容易从单线程调整到多线程。

FWIW,如果它是PIA,当你尝试单元测试它,然后它会PIA,当你尝试调试,bug修复或增强它。

首先,类及其协作者应该首先执行它们预期的目的,而不是关注依赖项。生命周期管理(实例何时创建和何时超出范围)不应该是类的责任。公认的最佳实践是制作或配置一个新组件来使用依赖注入来管理依赖项。

通常软件会变得更加复杂,拥有具有不同状态的Singleton类的多个独立实例是有意义的。在这种情况下,提交代码以简单地获取单例是错误的。对于小型简单系统来说,使用Singleton.getInstance()可能是可以的,但当可能需要同一个类的不同实例时,它不起作用/扩展。

任何类都不应该被认为是单例的,而应该是它的用法或它如何用于配置依赖项的应用程序。对于一个快速而讨厌的人来说,这并不重要——只是卢克硬编码说文件路径无关紧要,但对于更大的应用程序,这种依赖关系需要被分解出来,并使用DI以更合适的方式管理。

单例在测试中引起的问题是它们硬编码单一用例/环境的症状。测试套件和许多测试都是单独的,并且是单独的,与硬编码单例不兼容。

这是我认为迄今为止答案中缺少的:

如果您需要每个进程地址空间一个此对象的实例(并且您尽可能确信此需求不会改变),您应该将其设为单例。

否则,它不是单例。

这是一个非常奇怪的要求,用户几乎不感兴趣。进程和地址空间隔离是一个实现细节。只有当用户想使用kill或任务管理器停止您的应用程序时,它们才会影响用户。

除了构建一个缓存系统之外,没有太多原因可以让你如此确定每个进程只应该有某个东西的实例。日志系统怎么样?按线程或更细粒度可能更好,这样你就可以更自动地跟踪消息的来源。应用程序的主窗口怎么样?这取决于;出于某种原因,你可能希望所有用户的文档由同一个进程管理,在这种情况下,该进程中会有多个“主窗口”。

文斯·休斯顿有这些标准,对我来说似乎是合理的:

仅当满足以下所有三个标准时,才应考虑Singleton:

  • 单个实例的所有权无法合理分配
  • 懒惰初始化是可取的
  • 全局访问未另作规定

如果单个实例的所有权、初始化发生的时间和方式以及全局访问不是问题,那么Singleton就没有足够的兴趣。

我想谈谈被接受的答案中的4点,希望有人能解释为什么我错了。

  1. 为什么在代码中隐藏依赖关系不好?已经有几十种隐藏的依赖关系(C运行时调用、OS API调用、全局函数调用),单例依赖关系很容易找到(搜索实例())。

    “为了避免传递而使某些东西全局化是一种代码气味。”为什么传递某些东西以避免使其成为单例不是代码气味呢?

    如果你通过调用堆栈中的10个函数传递一个对象只是为了避免单例,那就太好了吗?

  2. 单一责任原则:我认为这有点模糊,取决于你对责任的定义。一个相关的问题是,为什么在类中添加这个特定的“责任”?

  3. 为什么将对象传递给类比将该对象用作类内的单例更紧密地耦合?

  4. 为什么它会改变状态持续的时间?单例可以手动创建或销毁,因此控件仍然存在,您可以使生命周期与非单例对象的生命周期相同。

关于单元测试:

  • 并非所有类都需要是单元测试
  • 不是所有的类都需要成为单元测试需要更改实现单例
  • 如果它们需要单元测试和确实需要更改实现,很容易把一个类从使用单例来具有Singleton通过依赖传递给它注入。

这里还有一件关于单身人士的事情,还没有人说过。

在大多数情况下,“单例性”是某个类的实现细节,而不是其接口的特征。控制容器反转可能会对类用户隐藏这个特征;你只需要将你的类标记为单例(例如在Java中有@Singleton注释),就是这样;其余的由IoCC完成。你不需要提供对单例实例的全局访问,因为访问已经由IoCC管理。因此IoC单例没有问题。

与IoC Singleton相反的GoF Singleton应该通过getInstance()方法在接口中暴露“单例性”,因此它们会受到上述所有内容的影响。

单例并不坏。只有当你做一些全球独一无二的东西时,它才是坏的。

但是,有“应用程序范围服务”(考虑一个使组件交互的消息传递系统)-这个CALLS用于单例,一个“MessageQueue”-类具有方法“SendMessage(…)”。

然后,您可以从各地执行以下操作:

SendMessage(new MailArrivedMessage(…));//发送邮件队列//当前消息

而且,当然,做:

消息队列。当前。注册接收器(此);

在实现IMessageRec的类中。

垄断是魔鬼,具有非只读/可变状态的单例是“真正的”问题。

在阅读了杰森的回答中建议的单身人士是病态的骗子之后,我遇到了这个小花絮,它提供了如何单例经常被误用的最佳示例。

全球不好是因为:

  • a.导致命名空间冲突
  • 它以一种毫无根据的方式暴露了国家

当涉及到Singletons时

  • a.调用它们的显式OO方式可以防止冲突,因此a点不是问题
  • 没有状态的单例(就像工厂一样)不是问题。具有状态的单例可以再次分为两类,不可变的或写一次并读取许多(配置/属性文件)。这些还不错。可变单例,就是你所说的引用持有者。

在最后的声明中,他指的是博客的“单身人士是骗子”的概念。

这如何适用于垄断?

要开始垄断游戏,首先:

  • 我们先制定规则,这样每个人都在同一页上
  • 每个人在比赛开始时都有一个平等的开始
  • 只有一套规则,以避免混淆
  • 规则在整个比赛中是不允许改变的

现在,对于任何没有玩过垄断的人来说,这些标准充其量是理想的。垄断的失败是很难接受的,因为垄断是关于钱的,如果你输了,你必须艰苦地看着其他玩家完成游戏,而损失通常是迅速和毁灭性的。所以,规则通常会在某个时候被扭曲,以牺牲其他人为代价来服务于一些玩家的自身利益。

所以你在和朋友鲍勃、乔和艾德玩垄断。你正在迅速建立你的帝国,并以指数级的速度消耗市场份额。你的对手正在削弱,你开始闻到血腥味(比喻地)。你的朋友鲍勃把他所有的钱都投入到尽可能多的低价值房产中,但他的投资回报并没有像他预期的那样高。鲍勃,运气不好,落在你的木板路上,被从游戏中剔除。

现在游戏从友好的掷骰子变成了严肃的生意。鲍勃已经成为失败的榜样,乔和埃德不想像“那个家伙”一样结束。所以,作为领先的玩家,你突然变成了敌人。乔和埃德开始练习台下交易,幕后注资,低估房屋交换,以及任何削弱你作为玩家的东西,直到他们中的一个上升到顶部。

然后,不是其中一人获胜,而是整个过程重新开始。突然之间,一组有限的规则变成了一个移动的目标,游戏退化成了一种社交互动,这种社交互动构成了自《幸存者》以来每一个高收视率真人秀节目的基础。为什么,因为规则正在改变,对于它们应该代表什么/为什么/没有达成共识,更重要的是,没有一个人做出决定。在这一点上,游戏中的每个玩家都在制定他/她自己的规则,混乱随之而来,直到两个玩家太累了,无法继续猜字谜,慢慢放弃。

因此,如果游戏规则手册准确地代表了单例,那么垄断规则手册就是滥用的一个例子。

这如何应用于编程?

除了可变单例存在的所有明显的线程安全和同步问题之外……如果你有一组数据,能够被多个不同的源同时读取/操作,并且在应用程序执行的生命周期内存在,那么可能是退后一步问“我在这里使用了正确类型的数据结构吗”的好时机。

就我个人而言,我见过一个程序员滥用单例,将其用作应用程序中某种扭曲的跨线程数据库存储。直接编写代码后,我可以证明它很慢(因为需要所有线程锁才能使其线程安全),是一个噩梦(因为同步错误的不可预测/间歇性),几乎不可能在“生产”条件下测试。当然,一个系统本可以使用轮询/信令来开发来克服一些性能问题,但这并不能解决测试的问题,当一个“真正的”数据库已经可以以更健壮/可扩展的方式完成相同的功能时,为什么还要费心呢?

如果您需要单例提供的内容,单例仅是一个选项。对象的一写只读实例。同样的规则也应该级联到对象的属性/成员。

从纯粹主义者的角度来看,单例是不好的。

从实际的角度来看,单例是开发时间与复杂性的权衡

如果您知道您的应用程序不会有那么大的变化,那么他们是可以接受的。只要知道如果您的需求以意想不到的方式变化(在大多数情况下这是非常好的),您可能需要重构东西。

单例有时也会使单元测试复杂化。

并不是单例本身不好,而是GoF设计模式不好。唯一有效的论点是GoF设计模式不适合测试,特别是当测试并行运行时。

使用类的单个实例是一个有效的构造,只要您在代码中应用以下方法:

  1. 确保将用作单例的类实现了一个接口。这允许使用相同的接口实现存根或模拟

  2. 确保Singleton是线程安全的。这是给定的。

  3. 单例本质上应该是简单的,而不是过于复杂。

  4. 在应用程序的运行时,需要将单例传递给给定对象,请使用构建该对象的类厂,并让类厂将单例实例传递给需要它的类。

  5. 在测试期间并为确保确定性行为,请将单例类创建为单独的实例,作为实际类本身或实现其行为的存根/模拟,并将其按原样传递给需要它的类。不要使用在测试期间创建需要单例的测试对象的类因素,因为它将传递它的单个全局实例,这与目的背道而驰。

我们在我们的解决方案中使用了Singleton,并取得了巨大的成功,这些解决方案是可测试的,确保了并行测试运行流中的确定性行为。

当涉及到聚类时,单例也很糟糕。因为那时,您的应用程序中不再有“只有一个单例”。

考虑以下情况:作为开发人员,您必须创建一个访问数据库的Web应用程序。为确保并发数据库调用不会相互冲突,您创建了一个线程保存SingletonDao

public class SingletonDao {// songleton's static variable and getInstance() method etc. omittedpublic void writeXYZ(...){synchronized(...){// some database writing operations...}}}

所以您确定应用程序中只存在一个单例,并且所有数据库都只通过这个SingletonDao。您的正式生产环境现在如下所示:单例

到目前为止一切都很好。

现在,考虑您想在集群中设置Web应用程序的多个实例。现在,你突然有了这样的东西:

多个单例

这听起来很奇怪,但是现在您的应用程序中有许多单例。这正是单例不应该的:拥有它的许多对象。如果您(如本示例所示)想要对数据库进行同步调用,这尤其糟糕。

当然,这是一个单例使用不当的例子。但这个例子的信息是:你不能指望你的应用程序中只有一个单例实例——尤其是在集群方面。

关于单例有多糟糕,我的答案总是“很难做对”。语言的许多基础组件都是单例(类、函数、命名空间,甚至是运算符),计算的其他方面的组件(localhost、默认路由、虚拟文件系统等)也是如此。这不是偶然的。虽然它们不时会带来麻烦和沮丧,但它们也可以让很多事情变得更好。

我看到的两个最大的错误是:把它当作一个全局的,没有定义Singleton闭包。

每个人都把Singleton的作为全局变量来谈论,因为它们基本上就是全局变量。然而,全局的很多(可悲的是,不是全部)坏处本质上并不源于全局,而是你如何使用它。Singleton也是如此。实际上,更重要的是,“单实例”真的不需要意味着“全局可访问”。它更像是一个自然的副产品,考虑到我们知道的所有坏处,我们不应该如此急于利用全局可访问性。一旦程序员看到一个Singleton,他们似乎总是通过它的实例方法直接访问它。相反,你应该像导航到任何其他对象一样导航到它。大多数代码甚至不应该意识到它正在处理一个Singleton(松耦合,对吧?)。如果只有一小部分代码访问对象,就像它是全局的一样,那么很多伤害就会被消除。我建议通过限制对实例函数的访问来强制执行它。

Singleton上下文也非常重要。Singleton的定义特征是“只有一个”,但事实是它在某种上下文/命名空间中“只有一个”。它们通常是:每个线程、进程、IP地址或集群一个,但也可以是每个处理器、机器、语言命名空间/类加载器/任何东西、子网、Internet等一个。

另一个不太常见的错误是忽略了Singleton的生活方式。仅仅因为只有一个并不意味着Singleton是万能的“过去和将来都是”,也不是通常可取的(没有开始和结束的对象违反了代码中各种有用的假设,应该只在最绝望的情况下使用。

如果你避免了这些错误,Singleton仍然可以是一个PITA,有点它已经准备好看到许多最糟糕的问题得到了显着缓解。想象一个JavaSingleton,它被明确定义为每个类加载器一次(这意味着它需要一个线程安全策略),具有定义的创建和销毁方法以及决定何时以及如何调用它们的生命周期,其“实例”方法具有包保护,因此通常可以通过其他非全局对象访问它。仍然是一个潜在的麻烦来源,但肯定麻烦少得多。

可悲的是,我们没有教好的例子如何做Singleton。我们教坏的例子,让程序员暂时使用它们,然后告诉他们这是一个糟糕的设计模式。

单例不是邪恶,如果你使用它适当最低。有很多其他好的设计模式在某种程度上取代了单例的需求(并且也提供了最好的结果)。但是一些程序员没有意识到这些好的模式,并在所有情况下使用单例,这使得单例对他们来说是邪恶的。

Singleton不是关于单个实例!

与其他答案不同,我不想谈论Singleton有什么问题,而是想向你展示它们在正确使用时是多么强大和令人敬畏!

  • 问题:单例在多线程环境中可能是一个挑战
    解决方案:使用单线程引导进程初始化单例的所有依赖项。
  • 问题:很难嘲笑单例。
    解决方案:使用方法工厂模式进行模拟

你可以将MyModel映射到继承它的TestMyModel类,当MyModel被注入时,你会得到TestMyModel的内联。-问题:单例可能会导致内存泄漏,因为它们从未处理过。
解决方案:好吧,处理它们!在您的应用程序中实现回调以正确处理单例,您应该删除链接到它们的任何数据,最后:从工厂中删除它们。

正如我在标题中所说,单例不是关于单实例的。

  • Singletons提高了易读性:您可以查看您的类并查看它注入了什么单例,以确定它的依赖项是什么。
  • 单例改进维护:一旦你从一个类中删除了一个依赖项,你就删除了一些单例注入,你不需要去编辑其他类的一个大链接,这些类只是移动了你的依赖项(这对我来说是臭代码@陈志立
  • 单例提高了内存和性能:当你的应用程序中发生了一些事情,并且需要很长的回调链来交付时,你正在浪费内存和性能,通过使用Singleton,你正在削减中间人,并提高你的性能和内存使用(通过避免不必要的局部变量分配)。