单身人士有什么不好呢?[关闭]


1982

单例模式是一个缴足成员四人帮模式书,但最近似乎而是由开发者世界孤立。我仍然使用很多单例,尤其是对于工厂类,尽管您必须对多线程问题(实际上是任何类)有所注意,但我看不出它们为什么如此糟糕。

堆栈溢出似乎特别假设每个人都同意Singletons是邪恶的。为什么?

请以“ 事实,参考或特定专业知识 ” 支持您的回答


6
我不得不说,在我尝试修改代码时,使用单例设计最近让我很烦。当我在空闲时间这样做时,我几乎懒得重构它。生产力的坏消息。
Marcin's

71
答案中有很多“缺点”,但我也想看看一些好的例子,说明模式何时好,与坏时形成对比……
DGM

50
几个月前,我写了一篇关于该主题的博客文章:jalf.dk/blog/2010/03/…-让我直接说一遍。我个人无法想到单例是正确解决方案的单一情况。这并不意味着不存在这种情况,但是……称其为稀有是轻描淡写。
jalf

8
@AdamSmith并不意味着您必须这样做,而是意味着您可以像这样访问它。而且,如果您不打算那样访问它,那么就没有理由将它放在首位。因此,您的论点实际上是:“如果我们不其视为单身人士,则制造单身人士没有任何危害。是的,太好了。如果我不开车,我的车也不会污染。但是,这样做更容易首先不要购买汽车。;)(全面披露:我实际上没有汽车)
jalf

35
整个主题中最糟糕的部分是,讨厌单身人士的人很少给出具体建议以代替使用什么。例如,贯穿本SO文章的期刊文章和自出版博客的链接继续说明为什么使用单例(这都是很好的原因),但是替代品却非常有限。虽然很多手挥手。我们中那些试图教给新程序员为什么不使用单例的人没有很多好的第三方反例来指出,只有人为的例子。好累
Ti Strga

Answers:


1292

布赖恩·巴顿(Brian Button)的释义:

  1. 它们通常用作全局实例,为什么这么糟?因为您在代码中隐藏了应用程序的依赖关系,而不是通过接口公开它们。使某些东西全局化以避免传递它是一种代码味道

  2. 他们违反了单一责任原则:由于他们控制自己的创作和生命周期。

  3. 它们固有地导致代码紧密耦合。在许多情况下,这使得将它们伪装成测试对象相当困难。

  4. 它们在应用程序的整个生命周期中都带有状态。测试的另一个问题是,您可能会遇到需要订购测试的情况,这对于单元测试来说是一个很大的不。为什么?因为每个单元测试都应该彼此独立。


324
恕我不能赞同。由于评论只允许使用600个字符,因此我写了一篇博客文章对此发表评论,请参见下面的链接。 jorudolph.wordpress.com/2009/11/22/singleton-considerations
Johannes Rudolph,2009年

61
在第1点和第4点,我认为单例非常有用,并且实际上几乎非常适合缓存数据(尤其是来自DB的数据)。性能的提高远远超过了建​​模单元测试所涉及的复杂性。
戴福

56
@Dai Bok:“高速缓存数据(尤其是从数据库中获取数据)”使用代理模式来实现...
paxos1977年

55
哇,反应很好。我在这里可能使用了过于激进的措辞,因此请记住,我在回答一个负面的问题。我的回答是简短列出因不良Singleton使用而引起的“反模式”。全面披露; 我也不时使用Singletons。关于SO的问题更加中立,这些问题将成为何时将Singletons视为好主意的绝佳论坛。例如,stackoverflow.com
questions /

21
它们对于在多线程环境中进行缓存不是很好。您可以通过多个线程争用有限资源来轻松击败缓存。[由单身人士维护]
僧侣,

449

单例解决一个(并且只有一个)问题。

资源争用。

如果您有一些资源

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

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

您需要一个单身人士

例子不多。日志文件很大。您不想只放弃一个日志文件。您要正确刷新,同步并关闭它。这是必须管理的单个共享资源的示例。

您很少需要单身人士。他们之所以不好,是因为他们感觉自己像个全球人,并且是GoF Design Patterns书中的全薪会员。

当您认为需要全局性时,您可能会犯下可怕的设计错误。


43
硬件也是一个例子吧?嵌入式系统有很多可能使用单例的硬件-也许是一个大的?<咧嘴>
Jeff

170
完全同意。有很多坚定的“这种做法是不好的”言论四处流传,而没有意识到这种做法可能有其地位。通常,这种做法只是“不好的”,因为它经常被滥用。只要应用适当,Singleton模式就没有本质上的错误。
Damovisa

53
真正的按原则的单例非常少见(并且打印机队列肯定不是一个,并且日志文件也不是-请参阅log4j)。通常,硬件是巧合而不是原则上的单例。连接到PC的所有硬件充其量都是巧合的(请考虑使用多个显示器,鼠标,打印机,声卡)。即使是500 mio $的粒子检测器,由于巧合和预算限制,也是单例-在软件中不适用,因此:没有单例。在电信中,电话号码和物理电话都不是单身人士(请考虑使用ISDN,呼叫中心)。
digitalarbeiter

23
硬件可能只有多种资源的一个实例,但是硬件不是软件。没有理由将开发板上的单个串行端口建模为Singleton。确实,对它进行建模只会使移植到具有两个端口的新板变得更加困难!
dash-tom-bang 2010年

19
因此,您要编写两个相同的类,重复很多代码,只是这样就可以让两个单例代表两个端口?仅使用两个全局SerialPort对象怎么样?根本不将硬件建模为类怎么样?为何要使用单例,这也意味着全局访问?您是否希望代码中的每个功能都能够访问串行端口?
jalf

352

一些编码小工具视它们为荣耀的全球对象。与许多人讨厌goto语句一样,其他人也讨厌曾经使用global的想法。我已经看到几个开发人员竭尽全力避免全局,因为他们考虑将开发人员视为失败的承认。奇怪但真实。

实际上,Singleton模式只是一种编程技术,它是概念工具包的有用部分。您可能会不时发现它是理想的解决方案,因此请使用它。但是使用它只是为了夸耀使用设计模式就像拒绝使用它一样愚蠢,因为它只是全局的


17
我在Singleton中看到的失败之处在于,人们使用它们而不是全局变量,因为他们在某种程度上“更好”。问题是(如我所见),在这些情况下,辛格尔顿带到餐桌上的东西是不相关的。(例如,即使未实际使用构造函数来进行首次构造,在非单一实例中实现它也是微不足道的。)
dash-tom-bang 2010年

21
“我们”看不起他们的原因是,“我们”经常看到他们用错了,而且经常使用。“我们”确实知道他们有其应有的地位。
Bjarke Freund-Hansen '02

5
@Phil,您说过“您可能会不时发现它是理想的解决方案,因此使用它”。好的,那么在哪种情况下我们会发现单例有用?
起搏器

22
@Pacerier,只要满足以下所有条件:(1)您只需要一种东西,(2)您需要在大量方法调用中将该东西作为参数传递,(3)您愿意接受可能有一天需要重构,以换取代码的大小和复杂性的即时降低,因为它不会在所有地方都散布这些令人毛骨悚然的东西。
antinome

18
我认为这并不能解决任何问题,只是说“有时可能合适,而其他时候可能不合适”。好的,但是为什么?何时?是什么使这个答案不仅仅是对节制争论
Guildenstern

219

来自Google的Misko Hevery就此主题发表了一些有趣的文章...

Singletons是Pathological Liars有一个单元测试示例,该示例说明了Singletons如何导致很难弄清依赖链以及启动或测试应用程序。这是虐待的一个极端例子,但是他提出的观点仍然有效:

单例仅是全局状态。全局状态使您的对象可以秘密地获取其API中未声明的内容,因此,Singletons使您的API陷入病态的骗子。

Singletons Gone的所有观点都表明,依赖注入使实例易于获得需要它们的构造函数,这减轻了第一篇文章中糟糕的全局Singletons背后的潜在需求。


8
Misko关于该主题的文章是目前最好的解决方法。
克里斯·梅耶

22
第一个链接实际上并没有解决单例问题,而是假设类内部存在静态依赖关系。可以通过传入参数来修复给定的示例,但仍然使用单例。
DGM

17
@DGM:确实-实际上,本文的“理性”部分与“单身人士是原因”部分之间存在巨大的逻辑脱节。
哈珀·谢尔比

1
Misko的CreditCard文章是滥用此模式的一个极端示例。
亚历克斯(Alex)

2
阅读第二篇文章Singletons实际上是所描述的对象模型的一部分。但是,它们不是工厂全局访问的,而不是全局访问的。
FruitBreak 2012年

121

我认为造成这种混乱的原因是人们不了解Singleton模式的实际应用。我不能太强调这一点。Singleton 不是包装全局变量的模式。单例模式仅应用于确保在运行时存在给定类的一个实例且只有一个实例

人们认为Singleton是邪恶的,因为他们将其用于全球。正是由于这种困惑,辛格尔顿被轻视了。请不要混淆Singleton和全局变量。如果将其用于预期目的,那么您将从Singleton模式中获得极大的好处。


20
该实例在整个应用程序中可用吗?哦。 全球。“ Singleton 不是包装全局变量的模式”要么是天真的,要么是误导性的,按照定义,该模式将类包装在全局实例周围。
cHao

26
当然,您可以在应用程序启动时自由创建类的一个实例,然后通过接口将该实例注入使用它的任何对象。该实现不关心只能有一个。
斯科特·惠特洛克

4
@Dainius:最后真的没有。当然,您不必随意将实例替换为另一个实例。(除了那些捂脸诱导时刻,当你做我确实遇到过。setInstance之前的方法。)这并不重要,尽管-这是威尼“被需要”一单也没有知道半导体封装的东西吓到或者有什么错可变的全局状态,因此他很有帮助(?)为每个人提供了设置器。单。领域。(是的,这确实发生了。很多情况。我在野外见过的几乎每个单身人士在设计上都是可变的,而且常常使人尴尬。)
cHao 2015年

4
@Dainius:在很大程度上,我们已经有。“比较喜欢继承而不是继承”已经有一段时间了。当继承 demonstrably最好的解决方案,不过,你当然可以自由使用它。与单身,全局变量,线程,同样的事情goto,等他们可能工作在很多情况下,但坦率地说,“作品”是不够的-如果你想违背传统智慧,你最好能够证明如何你的方法为更好地比传统的解决方案。而且我还没有看到Singleton模式的这种情况。
cHao 2015年

3
以免我们彼此交谈,我不仅在谈论全球可用的实例。有很多情况。我要说的(尤其是当我说“ capital-S Singleton”时)是GoF的Singleton模式,该模式将单个全局实例嵌入到类本身中,通过getInstance或类似名称的方法公开它,并防止a的存在。第二审。坦白说,到那时,您甚至可能没有一个实例。
cHao 2015年

72

关于单例的一件相当不好的事情是,您不能很轻松地扩展它们。如果要更改其行为,则基本上必须构建某种装饰器模式或类似的东西。同样,如果有一天您想以多种方式完成某件事,那么根据您对代码的布局方式进行更改可能会非常痛苦。

需要注意的一件事是,如果您确实使用单例,请尝试将其传递给需要它们的人,而不是让他们直接访问它。否则,如果您选择采用多种方式来完成单例,那么改变非常困难,因为如果每个类都直接访问单例,则会嵌入一个依赖项。

所以基本上:

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

而不是:

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

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

就像任何模式一样...考虑它,并考虑在给定情况下使用它是否不合适...通常会违反规则,并且不应该无意地应用这些模式


17
呵呵,如果您到处都这样做,那么您也将不得不在各处都引用对singelton的引用,因此您不再有单身人士了。:)(这通常是IMO的好事。)
Bjarke Freund-Hansen 2012年

2
@ BjarkeFreund-Hansen-废话,你在说什么?单例只是一个实例化一次的类的实例。引用这样的Singleton并不会复制实际的对象,它只是引用它-您仍然拥有相同的对象(阅读:Singleton)。
M. Mimpen 2013年

2
@ M.Mimpen:不,大写的S Singleton(此处正在讨论)是一个类的实例,该类(a)保证只存在一个实例,并且(b)通过该类自己的类进行访问内置的全局访问点。如果您声明不应该调用getInstance(),则(b)不再是真的。
cHao 2014年

2
@cHao我没有关注您,或者您不了解我向谁发表评论-这是Bjarke Freund-Hansen的。Bjarke指出,具有多个单例引用会导致具有多个单例。这肯定是不正确的,因为没有深层副本。
M. Mimpen

6
@ M.Mimpen:我接受他的评论,以更多地提及语义效果。一旦您取消对的调用getInstance(),您就可以有效地消除Singleton模式与普通引用之间的一个有用区别。就其余的代码而言,单一性不再是一个属性。只有getInstance()永远的调用者才需要知道甚至关心多少个实例。仅使用一个调用者,使类可靠地实现单一性要比使调用者简单地存储引用并重用它要付出更多的努力和灵活性。
cHao 2014年

69

单例模式本身不是问题。问题在于,这种模式通常由人们使用面向对象的工具来开发软件,而没有扎实地理解OO概念。在这种情况下引入单例时,它们往往会成长为难以管理的类,其中包含每次使用很少的辅助方法。

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

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

在多线程方案中,单例也可能成为瓶颈以及同步问题。


4
我知道这是很多岁的线程。您好@Kimoz您说:-单例很快会成为内存管理方面的问题。我想更详细地说明有关单例和垃圾回收的问题。
托马斯

@Kimoz,问题是:“为什么单例模式本身不是问题?” 并且您只重复了这一点,但没有提供单例模式的单个有效用例。
起搏器

@Thomas,因为根据定义,单例仅存在一个实例。因此,将唯一的引用分配给null通常很复杂。可以这样做,但这意味着您可以完全控制在应用程序中不使用单例的点。这种情况很少见,通常与单身爱好者所寻找的相反:一种使单个实例始终可访问的简单方法。在诸如Guice或Dagger之类的DI框架上,不可能摆脱单例,并且它会永远保留在内存中。(尽管提供的集装箱单件行李要远远优于自制集装箱)。
尼克斯(Snicolas)

54

单例使用静态方法实现。进行单元测试的人员应避免使用静态方法,因为它们不能被模拟或存根。该站点上的大多数人都大力支持单元测试。避免它们的最普遍接受的约定是使用控制模式的反转


9
这听起来更像是单元测试的问题,它可以测试对象(单元),功能(单元),整个库(单元),但由于类中的任何静态内容(也包括单元)而失败。
v010dya 2014年

2
难道您不是想将所有外部参考资料都保留下来吗?如果是,那么moc singleton有什么问题,如果不是,那么您真的在进行单元测试吗?
Dainius

@Dainius:模拟实例比模拟类麻烦得多。可以想象,您可以从应用程序的其余部分中提取被测类,并使用伪Singleton类进行测试。但是,这极大地增加了测试过程。首先,您现在需要能够随意卸载类(在大多数语言中并不是真正的选择),或者为每个测试启动一个新的VM(阅读:测试可能要花费数千倍的时间)。但是,有两个依赖关系Singleton是一个实现细节,该细节现在正在整个测试中泄漏。
cHao 2015年

Powermock可以模拟静态内容。
尼克斯(Snicolas)

模拟对象意味着创建真实对象吗?如果模拟不能创建真实的对象,那么为什么类是单例还是方法是静态的又为什么呢?
阿伦·拉杰

45

集群而言,单例也很糟糕。因为这样,您的应用程序中不再有“恰好一个单例”。

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

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

因此,您可以确保应用程序中仅存在一个单例,并且所有数据库都通过该单例SingletonDao。现在,您的生产环境如下所示: 单身单身

到目前为止一切都很好。

现在,考虑要在群集中设置Web应用程序的多个实例。现在,您突然有了以下内容:

许多单身人士

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

当然,这是单例使用不当的一个例子。但是此示例的信息是:您不能依靠您的应用程序中只有一个单例实例-尤其是在集群方面。


4
如果您不知道如何实现单例,则不应这样做。如果您不知道自己在做什么,则应该首先找到它,然后再找到所需的东西。
Dainius

3
这很有趣,我有一个问题。那么,如果单例(每个都位于不同的机器/ JVM上)正在连接到单个数据库,那到底是什么问题呢?Singletons范围仅适用于该特定的JVM,即使在集群中也是如此。不仅仅是从哲学上说这种特殊情况很糟糕,因为我们的意图是整个应用程序中的单个对象,我很高兴看到由于这种安排而可能出现的任何技术问题。
Saurabh Patil

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

33

垄断是魔鬼,具有非只读/可变状态的单身人士是``真正的''问题...

在阅读了杰森的答案中建议的“ 单身人士是病态骗子”之后,我遇到了这个小窍门,它提供了关于单身人士经常被滥用的最佳例子。

全局是不好的,因为:

  • 一个。它导致名称空间冲突
  • b。它以不必要的方式暴露了国家

说到单例

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

在上一次声明中,他指的是博客的“单身是骗子”的概念。

这如何适用于专卖?

要开始垄断游戏,首先:

  • 我们首先建立规则,以便每个人都在同一页面上
  • 在游戏开始时,每个人都有平等的起点
  • 仅提出一组规则以避免混淆
  • 在整个游戏中不允许更改规则

现在,对于没有真正扮演垄断者的人,这些标准充其量是最理想的。垄断的失败很难吞噬,因为垄断就是金钱,如果您输了,您就必须努力地看着其余的球员完成比赛,而损失通常是迅速而令人沮丧的。因此,规则通常在某些时候会扭曲,以牺牲某些玩家的利益为某些玩家的自身利益服务。

因此,您正在与朋友Bob,Joe和Ed垄断。您正在迅速建立自己的帝国,并以指数级的速度占领市场份额。您的对手正在减弱,您开始闻到血腥味(象征性地)。您的好友鲍勃(Bob)将所有资金投入了尽可能多的低价值房产,但他没有得到他所期望的高投资回报。鲍勃(Bob)碰运气不佳,落在您的木板路上,并从游戏中删除。

现在,游戏从掷骰子到认真做生意。鲍勃已经成为失败的榜样,乔和埃德不想像“那个家伙”那样结局。因此,作为领先者,您突然变成了敌人。乔和埃德开始练习桌下交易,后台注资,低估房屋交易,以及通常会削弱您作为一名玩家的任何东西,直到其中之一升至最高点。

然后,而不是其中一个获胜,整个过程重新开始。突然,有限的规则集成为了移动目标,游戏逐渐退化为社交互动类型,这构成了自《生还者》以来每部高质量真人秀节目的基础。为什么,因为规则在不断变化,人们对如何代表/为什么/代表什么没有共识,更重要的是,没有人做出决定。那时,游戏中的每个玩家都在制定自己的规则和混乱,直到其中两个玩家过于疲倦以至于无法保持自我并慢慢放弃。

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

这如何适用于编程?

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

就个人而言,我已经看到程序员滥用单例,将其用作应用程序中某种形式的扭曲的跨线程数据库存储。直接处理代码后,我可以证明它运行缓慢(由于要使其具有线程安全性,需要进行所有线程锁定)和工作的噩梦(由于同步错误的不可预测/间歇性),以及在“生产”条件下进行测试几乎是不可能的。当然,本来可以使用轮询/信令开发系统来解决某些性能问题,但是这不能解决测试问题,以及为什么当真正的数据库已经可以以更强大的功能完成相同功能时,何必打扰呢? /可扩展方式。

单身是唯一的选择,如果你需要什么单提供。对象的写一次只读实例。相同的规则也应级联到对象的属性/成员。


1
如果单身人士兑现一些数据怎么办?
Yola 2014年

@Yola如果按预定和/或固定的时间表计划更新单例,它将减少写入次数,从而减少线程争用。但是,除非模拟了模拟相同用法的非单实例,否则您将无法准确测试与单例交互的任何代码。TDD的人们可能会适应,但是会奏效。
Evan Plaice 2014年

@EvanPlaice:即使使用了模拟程序,您的代码也会遇到一些问题Singleton.getInstance()。支持反射的语言可能可以通过设置一个真实实例的存储字段来解决此问题。但是,一旦IMO绕过另一类的私有状态,IMO的测试就变得不那么值得信赖了。
cHao 2015年

28

单例与单实例无关!

与其他答案不同,我不想谈论Singletons的问题所在,而是向您展示正确使用它们时它们的功能和强大程度!

  • 问题:单例在多线程环境中可能是一个挑战
    解决方案:使用单线程引导程序进程初始化单例的所有依赖关系。
  • 问题:很难模拟单身人士。
    解决方案:使用方法Factory模式进行模拟

您可以映射MyModelTestMyModel继承它的类,在任何时候MyModel注入它们都会使您了解TestMyModel。- 问题:单例可能会导致内存泄漏,因为它们永远不会被丢弃。
解决方案:好,处置它们!在您的应用中实现回调以正确处理单例,您应该删除链接到它们的所有数据,最后:从工厂中删除它们。

正如我在标题中所述,单例与单个实例无关。

  • Singletons提高了可读性:您可以查看您的类,并查看它注入了什么singleton以确定其依赖项。
  • Singletons改善了维护性:从类中删除依赖项后,您只需删除一些Singleton注入,就无需去编辑其他类的大型链接,这些链接只是将您的依赖项移到了周围(这对我来说是很臭的代码@Jim Burger
  • Singletons可以提高内存和性能:当应用程序中发生某些事情,并且需要一长串回调来传递时,您浪费了内存和性能,通过使用Singleton削减了中间人,并提高了性能和内存使用率(通过避免不必要的局部变量分配)。

2
这不能解决我的Singletons的主要问题,因为Singletons允许您从项目中的数千个类中的任何一个访问全局状态。
LegendLength

8
那就是目的……
Ilya Gazman '16

LegendLength为什么会出错?例如,在我的应用程序中,我有一个单例Settings对象,可从任何小部件访问该对象,以便每个小部件都知道如何格式化显示的数字。如果它不是全局可访问的对象,则必须将其在构造函​​数中注入到构造函数中的每个小部件,并将对它的引用保留为成员变量。这是对内存和时间的可怕浪费。
VK

23

我对Singletons如何不好的回答总是,“他们很难做对”。语言的许多基本组件都是单例(类,函数,名称空间甚至是运算符),而其他计算方面的组件(本地主机,默认路由,虚拟文件​​系统等)也是如此,这并非偶然。尽管它们有时会引起麻烦和沮丧,但它们也可以使很多事情变得更好。

我看到的两个最大的问题是:将其视为全局对象,并且未能定义Singleton闭包。

每个人都将Singleton视为全局变量,因为它们基本上是全局变量。但是,全局中的很多(并不是全部)坏处本质上不是来自于全局,而是您如何使用它。Singletons也是如此。实际上,更多的是“单个实例”,实际上不需要表示“全局可访问”。它是一种自然的副产品,鉴于我们所知道的所有弊端,我们不应该急于利用全球可访问性。一旦程序员看到了Singleton,他们似乎总是总是通过其实例方法直接访问它。相反,您应该像浏览其他任何对象一样导航到该对象。大多数代码甚至都不应该知道它正在处理Singleton(松耦合,对吗?)。如果只有一小段代码像访问全局对象那样访问该对象,则将带来很多危害。

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

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

如果您避免了这些错误,那么Singletons仍然可以成为PITA,可以随时看到许多最严重的问题都得到了缓解。想象一下一个Java Singleton,它被明确定义为每个类加载器一次(这意味着它需要线程安全策略),具有定义的创建和销毁方法以及一个生命周期,该生命周期规定了何时以及如何调用它们,其“实例”方法具有软件包保护,因此通常可以通过其他非全局对象进行访问。仍然是潜在的麻烦源,但麻烦肯定要少得多。

可悲的是,与其讲授如何做单例的好例子。我们教一些不好的例子,让程序员先用一段时间,然后告诉他们他们是不好的设计模式。


23

参见维基百科Singleton_pattern

某些人认为它已经被过度使用,因而认为它是反模式,在实际上不需要类的唯一实例的情况下引入了不必要的限制。[1] [2] [3] [4]

参考(仅与文章相关的参考)

  1. ^亚历克斯·米勒。我讨厌的模式1:Singleton,2007年7月
  2. ^ Scott Densmore。为什么单身人士是邪恶的,2004年5月
  3. ^史蒂夫·耶格。2004年9月,单身汉被认为是愚蠢的
  4. ^ JB Rainsberger,IBM。明智地使用单身人士,2001年7月

8
对模式的描述并不能解释为什么它被认为是邪恶的……
Jrgns

2
几乎不公平:“某些人也认为它是反模式,他们认为它使用过度,在实际上不需要类的唯一实例的情况下引入了不必要的限制。” 看看参考文献...反正我有RTFM。
GUI Junkie

17

并非单例本身很糟糕,而是GoF设计模式。唯一有效的论点是GoF设计模式不适合进行测试,尤其是在并行运行测试的情况下。

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

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

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

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

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

  5. 在测试过程中,为了确保确定性行为,将单例类作为单独的实例创建为实际类本身或实现其行为的存根/模拟,并将其按原样传递给需要它的类。不要使用创建要在测试期间需要单例的被测对象的类因子,因为它将通过对象的单个全局实例,这会达到目的。

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


+1,最后是解决单例何时有效的答案。
Pacerier 2014年

16

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

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

    “使全局事物避免传递出去是一种代码气味。” 为什么不传递某些内容以避免将其变成单例代码的味道?

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

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

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

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

关于单元测试:

  • 并非所有类都需要进行单元测试
  • 并非所有需要进行单元测试的类都需要更改单例的实现
  • 如果确实需要对它们进行单元测试并且确实需要更改实现,则很容易将类从使用单例更改为通过依赖注入将单例传递给它。

1
1.所有那些隐藏的依赖项? 那些也不好。 隐藏的依赖始终是邪恶的。但是对于CRT和OS,它们已经存在,并且它们是完成某项工作的唯一方法。(尝试编写不使用运行时或OS的C程序。)这种做事的能力大大超过了它们的缺点。我们代码中的单例并没有那么奢侈。由于它们完全处于我们的控制和责任范围之内,因此,每种用法(请参阅:每个其他隐藏的依赖项)都应该是合理的,是完成工作的唯一合理方法。非常,非常少数人实际上是。
cHao 2013年

2.“责任”通常定义为“变更原因”。单身人士最终要管理自己的生命周期并完成实际工作。如果更改工作的完成方式对象图的构建方式,则必须更改类。但是,如果您有其他代码构建对象图,并且您的类仅完成其实际工作,则该初始化代码可以按自己喜欢的方式设置对象图。一切都更加模块化和可测试,因为您可以插入测试双打并随意撕毁所有内容,而不再依赖隐藏的运动部件。
cHao 2013年

1
@cHao:1.很好的是,您认识到“做事的能力大大超过了他们的负面影响”。一个示例是日志记录框架。如果这意味着不鼓励开发人员进行日志记录,则强制通过依赖注入通过函数调用层传递全局日志记录对象是邪恶的。因此,单身人士。3.只有当您希望该类的客户通过多态性控制行为时,紧密的耦合点才有意义。通常情况并非如此。
2013年

2
4.我不确定“不能手动销毁”是否是“只能是一个实例”的逻辑含义。如果那是您定义单例的方式,那么我们不是在谈论同一件事。所有类都不应该进行单元测试。这是一项业务决策,有时上市时间或开发成本将优先考虑。
2013年

1
3.如果您不希望多态,那么您甚至不需要实例。您也可以将其设置为静态类,从而使您始终将自己绑定到单个对象和单个实现上-至少这样,API不会对您造成太大的欺骗。
cHao

15

文斯·休斯顿(Vince Huston)具有以下标准,在我看来,这是合理的:

仅当满足以下所有三个条件时,才应考虑单例:

  • 无法合理分配单个实例的所有权
  • 延迟初始化是可取的
  • 否则不提供全局访问

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


14

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

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

如果您知道您的应用程序不会改变太多,那么它们就可以了。只是知道,如果您的需求以意想不到的方式发生变化(在大多数情况下还可以),则可能需要重构。

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


3
我的经验是,即使从短期来看,单例开始也确实会伤害您,而不是从长远来看。如果应用程序已经存在了一段时间,并且可能已经被其他单例感染了,那么这种影响可能就不那么明显了。从头开始?不惜一切代价避免它们!想要一个对象的单个实例吗?让工厂管理该对​​象的实例化。
斯文

1
AFAIK Singleton 工厂方法。因此,我没有得到您的推荐。
tacone

3
“一个工厂”!=“一个不能与同一个类中的其他有用东西分开的factory_method”
斯文

13

假设模式已用于真正单一的模型的某个方面,那么模式就没有本质上的错误。

我认为,这种抵制是由于过度使用,而这又是因为它是最容易理解和实施的模式。


6
我认为这种反弹更多与实践正式的单元测试的人们有关,他们意识到自己是一个噩梦。
Jim Burger

1
不确定为什么是-1。这不是最具描述性的答案,但他也没有错。
Outlaw程序员

13

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


3
Spring bean在默认情况下不是Singletons吗?
苗条

3
是的,但我的意思是“编码”单身人士。我还没有“写一个单例”(即按照设计模式使用私有构造函数yada yada yada)-是的,在春季,我使用的是单个实例。
prule 2010年

4
因此,当其他人这样做时,也可以(如果以后再使用),但是如果其他人这样做并且我将不使用它,那是邪恶的吗?
Dainius

11

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

单例的坏部分通常是用户(或者我应该说,单例不适合其设计用途的不适当使用)。最大的罪犯是使用单例作为伪造的全局变量。


1
谢谢洛基:)正是我的意思。整个狩猎过程使我想起了goto辩论。工具是为知道如何使用它们的人而设计的。使用您不喜欢的工具可能会对您造成危险,因此请避免使用它,但不要告诉他人不要/不要学习正确使用它。我并不完全是“ pro-singleton”,但我喜欢这样的事实,我可以感觉到在哪里使用它是合适的。期。
dkellner 2015年

8

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

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

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

而不是使用单例,您应该在函数之间传递所有需要的实用程序对象。如果将它们全部包装到一个辅助对象中,则可以简化操作,如下所示:

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

4
每个方法的传递参数都很聪明,它至少应该介绍如何污染api的十大方法。
Dainius

这是一个明显的缺点,应该通过某种方式处理该语言,方法是将参数以某种方式传播到嵌套调用中,但是如果没有这种支持,则必须手动完成。考虑一下,即使是自然的单例,例如代表系统时钟的对象,也有可能带来麻烦。例如,您如何用测试覆盖与时钟相关的代码(认为是多价电表)?
Roman Odaisky

取决于您要测试的内容,如果您不测试单例,为什么要关心它的行为,如果您的2个远程对象彼此依赖,则不是单例问题
。– Dainius

您能提供哪种语言会传播到嵌套调用中吗?
Dainius

1
如果您有一个模块依赖于另一个模块,并且您不知道/无法移动它,则无论是否单例,您都会遇到困难。人们经常在应该使用合成的地方使用继承方式,但是您并不是要说继承是不好的,应该不惜一切代价避免OOP,因为那里有很多人在做设计错误,还是您?
Dainius

8

单身人士的问题是范围扩大并因此耦合的问题。不可否认,在某些情况下您确实需要访问单个实例,并且可以通过其他方法来实现。

我现在更喜欢围绕控制反转(IoC)容器进行设计,并允许生命周期由容器控制。这为您提供了依赖实例的类的好处,而不必知道只有一个实例。单例的生存期可以在将来更改。我最近遇到的这样的例子是从单线程到多线程的简单调整。

FWIW,如果在尝试对其进行单元测试时是PIA,则在尝试对其进行调试,错误修复或增强时将转到PIA。



7

单身人士还不错。仅当您使某项全局唯一性变为非全局唯一性时,这才很糟糕。

但是,存在“应用程序范围服务”(想一想使组件进行交互的消息传递系统)-一个单例的此CALLS,即“ MessageQueue”-具有方法“ SendMessage(...)”的类。

然后,您可以从各处进行以下操作:

MessageQueue.Current.SendMessage(new MailArrivedMessage(...));

并且,当然,请执行以下操作:

MessageQueue.Current.RegisterReceiver(this);

在实现IMessageReceiver的类中。


6
如果我想创建一个范围更小的第二个消息队列,为什么不应该允许我重复使用您的代码来创建它呢?单身人士可以防止这种情况。但是,如果您只是创建了一个常规消息队列类,然后创建了一个全局实例来充当“应用程序范围”,那么我可以创建另一个实例供其他用途。但是,如果您将类设为单例,则必须编写第二个消息队列类。
jalf

1
还有为什么我们不应该只拥有一个全局的CustomerOrderList以便我们可以从MessageQueue这样的任何地方很好地调用它呢?我相信两者的答案都是相同的:它有效地产生了一个全局变量。
LegendLength

7

太多的人将不是线程安全的对象放在单例模式中。尽管DataContext不是线程安全的并且纯粹是一个工作单元对象,但我已经看到了以Singleton模式完成的DataContext(LINQ to SQL)示例。


许多人编写不安全的多线程代码,这是否意味着我们应该消除线程?
Dainius,2014年

@Dainius:事实上,是的。IMO的默认并发模型应该是多进程的,并允许两个进程(1)轻松地相互传递消息,和/或(2)根据需要共享内存。如果您想共享所有内容,则线程化很有用,但是您从不真正想要那样做。可以通过共享整个地址空间来模拟它,但这也被认为是一种反模式。
cHao 2015年

@Dainius:当然,这是假设根本需要诚实到善良的并发。通常,您想要做的只是能够在等待操作系统做其他事情的同时做一件事。(例如,在从套接字读取时更新UI。)在大多数情况下,体面的异步API可能使全线程不再需要。
cHao 2015年

所以如果你有巨大的数据矩阵,你需要处理,这是更好地做,在单个线程,因为很多人可能会做,错了..
纽斯

@Dainius:在很多情况下,是的。(如果将同步添加到混合中,线程实际上可能会使您慢下来。这是为什么多线程不应该成为大多数人首先想到的一个很好的例子。)在其他情况下,最好让两个进程仅共享一个所需的东西。当然,您必须安排进程共享内存。但坦率地说,我认为这是一个很好的事情-要好得多,至少,不是默认共享内容模式。它要求您明确说出(因此,理想情况下,要知道)哪些部分是共享的,因此哪些部分需要线程安全。
cHao 2015年

6

这是关于单身人士的又一件事,但没人说过。

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

与IoC Singletons相反的GoF Singletons应该通过getInstance()方法在接口中公开“单一性”,从而使它们遭受上述问题的困扰。


3
在我看来,“单一性”是运行时环境的详细信息,编写类代码的程序员不应考虑“单一性”。而是由类USER考虑。只有USER知道实际需要多少个实例。
Earth Engine

5

如果您正确最少使用它,那么单例并不是邪恶的。还有许多其他好的设计模式可以在某个时候替代单例的需求(并且也可以提供最佳结果)。但是有些程序员没有意识到那些好的模式,并在所有情况下都使用单例,这使单例对他们有害。


太棒了!完全同意!但是您可以并且也许应该详细说明更多。例如扩展哪些设计模式最常被忽略,以及如何“适当和最少”使用单例。说起来容易做起来难!:P
cregox

基本上,替代方法是通过方法参数传递对象,而不是通过全局状态访问对象。
LegendLength

5

首先,班级及其合作者应首先实现其预期目的,而不是专注于家属。生命周期管理(何时创建实例以及何时超出范围)不属于类职责。公认的最佳实践是使用依赖项注入来制作或配置一个新组件来管理依赖项。

通常,软件会变得更加复杂,因此拥有多个具有不同状态的Singleton类的独立实例是有意义的。在这种情况下,提交代码以简单地抓住单例是错误的。Singleton.getInstance()对于小型简单系统而言,使用可能没问题,但是当可能需要同一类的不同实例时,使用这种方法就行不通。

不应将任何类视为单例,而应该将其用作其用法或如何使用它来配置依赖项。快速而讨厌这没关系-只是硬编码说文件路径并不重要,但对于较大的应用程序,则需要使用DI以更适当的方式分解并管理这种依赖关系。

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


4

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


如果您的课程不限于一次实例,您将需要由信号量管理的班级中的静态成员,这几乎是同一件事!您建议的替代方案是什么?
Jeach 2010年

我有一个带有“ MainScreen”的大型应用程序,该屏幕打开了许多较小的模态/非模态窗口/ UI窗体。恕我直言,我认为MainScreen应该是一个单例,例如,在应用程序远处某个位置的小部件要在MainScreen的状态栏中显示其状态,它要做的就是MainScreen.getInstance()。setStatus( “某些文字”);您有什么建议呢?通过MainScreen遍及整个应用程序?:D
萨尔文·弗朗西斯

2
@SalvinFrancis:我推荐的是,您不再拥有关心他们不关心的东西的对象,而是偷偷溜走整个应用程序以使彼此混乱。:)您的示例将更好地处理事件。当您正确地处理事件时,小部件甚至不必关心是否有MainScreen。它只是广播“嘿,发生了事情”,而订阅了“发生事情”事件的任何事件(无论是MainScreen,WidgetTest还是完全其他事件!)都决定了它想如何响应。无论如何,这就是应该执行OOP的方式。:)
cHao 2013年

1
@Salvin考虑一下,如果许多组件正在“默默地”更新MainScreen,那么在调试MainScreen时就很难推理。您的示例是单例错误的完美原因。
LegendLength
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.