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


单身模式 是一个完全付清的成员 四人帮模式书,但它最近似乎是开发者世界的孤儿。我仍然使用相当多的单身人士,尤其是 工厂类虽然你必须对多线程问题(实际上是任何类)有点小心,但我不明白为什么它们太可怕了。

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

请用“支持你的答案”事实,参考或具体的专业知识


1737


起源


我不得不说,最近使用单件设计让我很烦,因为我试图调整代码。当我在空闲时间这样做时,我几乎懒得重构这个。生产力的坏消息。 - Marcin
答案中有很多“缺点”,但我也希望看到一些很好的例子,说明模式何时好,与坏的对比... - DGM
几个月前我写了一篇关于这个主题的博客文章: jalf.dk/blog/2010/03/...  - 让我直接说出来。我个人无法想到单身人士是正确的解决方案。这并不意味着这种情况不存在,但......称它们是罕见的是轻描淡写。 - jalf
@AdamSmith它并不代表你 有 到,但它意味着你 能够 像那样访问它。如果你不打算像那样访问它,那么就没有理由让它成为一个单身人士。所以你的论点实际上是“如果我们不这样做,那么制作一个单身者就没有害处 对待 它作为一个单身人士。很好。如果我不开车,我的车也不会污染。但是,一开始就不容易购买汽车。 ;)(完全披露:我实际上没有车) - jalf
整个主题中最糟糕的部分是,讨厌单身人士的人很少提出具体的建议来代替使用什么。例如,通过这篇SO文章链接到期刊文章和自我发布的博客,继续讨论原因 不 使用单身人士(而且他们都是很好的理由),但他们在替补上非常苗条。尽管如此,还是有很多人在忙。我们这些试图教新程序员为什么不使用单身人士的人并没有很多好的第三方反例,只有人为的例子。这很烦人。 - Ti Strga


答案:


从Brian Button转述:

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

  2. 他们违反了 单一责任原则:凭借他们控制自己的创造和生命周期的事实。

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

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


1148



恕我不能赞同。由于评论只允许600个字符,我写了一篇博客文章对此发表评论,请看下面的链接。 jorudolph.wordpress.com/2009/11/22/singleton-considerations - Johannes Rudolph
在第1点和第4点,我认为单例是有用的,实际上几乎完美的缓存数据(特别是来自数据库)。性能的提高远远超过了建​​模单元测试所涉及的复杂性。 - Dai Bok
@Dai Bok:“缓存数据(特别是来自数据库)”使用代理模式来执行此操作... - paxos1977
哇,很好的回应。我可能在这里使用了过于激进的措辞,所以请记住我正在回答一个负面的问题。我的回答是一个简短的列表,列出了因单独使用不良而产生的“反模式”。全面披露;我也经常使用Singletons。关于SO的问题还有更多中立的问题,这些问题可以成为单身人士被认为是个好主意的绝佳论坛。例如, stackoverflow.com/questions/228164/... - Jim Burger
它们在多线程环境中的缓存效果不是很好。您可以通过多个线程在有限的资源上进行战斗来轻松击败缓存。 [由单身人士维持] - monksy


单身人士解决了一个(也是唯一一个)问题。

资源争用。

如果你有一些资源

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

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

你需要一个 独生子

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

你很少需要一个单身人士。他们不好的原因是他们感觉像是一个 全球 他们是GoF的全额支付成员 设计模式 书。

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


399



硬件也是一个例子吗?嵌入式系统有很多可能使用单例的硬件 - 或者可能是一个大单元? <笑容> - Jeff
完全同意。有很多坚定的“这种做法是坏的”谈话浮出水面而没有任何认识到这种做法可能取而代之。通常,这种做法只是“糟糕”,因为它经常被滥用。 Singleton模式没有任何内在错误,只要它被适当地应用。 - Damovisa
真正的,原则上的单例非常罕见(打印机队列当然不是一个,也不是日志文件 - 请参阅log4j)。通常情况下,硬件是巧合而不是原则的单身。连接到PC的所有硬件充其量只是一个巧合的单例(想想多个显示器,鼠标,打印机,声卡)。即使是一个500万美元的粒子探测器也只是巧合和预算约束的单例 - 它们不适用于软件,因此:没有单例。在电信领域,电话号码和手机都不是单身人士(想想ISDN,呼叫中心)。 - digitalarbeiter
硬件可能只有一个各种资源的实例,但硬件不是软件。没有理由将开发板上的单个串行端口建模为Singleton。实际上,对其进行建模只会使端口更难以移植到具有两个端口的新板上! - dash-tom-bang
所以你要写两个相同的类,复制很多代码,这样你就可以有两个代表两个端口的单例?如何使用两个全局SerialPort对象?如何不将硬件建模为类?为什么你会使用单身,这也意味着全球访问?你要吗 一切 您的代码中的函数能够访问串口吗? - jalf


一些编码的势利者瞧不起他们只是一个美化的全球。就像许多人讨厌的那样  声明还有其他人讨厌曾经使用过的想法 全球。我见过几个开发人员为了避免a 全球 因为他们考虑使用一个作为承认失败。奇怪但真实。

在实践中 独生子 模式只是一种编程技术,是您的概念工具包的有用部分。您可能会不时发现它是理想的解决方案,因此请使用它。但是使用它只是为了让你自夸使用a 设计模式 就像拒绝使用它一样愚蠢,因为它只是一个 全球


307



我在Singleton中看到的失败是人们使用它们而不是全局因为它们在某种程度上“更好”。问题是(正如我所看到的),在这些情况下,Singleton带来的东西是无关紧要的。 (例如,构造在第一次使用时非常容易在非单例中实现,即使它实际上并没有使用构造函数来执行它。) - dash-tom-bang
“我们”瞧不起他们的原因是“我们”经常看到他们使用错误,而且经常使用。 “我们”确实知道他们有自己的理由。 - Bjarke Freund-Hansen
@Phil,你说“你可能会不时发现它是理想的解决方案,所以使用它”。好吧,确切地说 哪一个 我们会发现一个单例有用吗? - Pacerier
@Pacerier,只要满足以下所有条件:(1)你只需要一个东西,(2)你需要在大量的方法调用中将那一个东西作为参数传递,(3)你愿意接受一个有一天不得不重构的机会,以换取立即减少你的代码的大小和复杂性,不要在任何地方传递令人难忘的东西。 - antinome
我不认为这回答任何问题,它只是说'有时它可能适合,有时可能不适合'。好的,但为什么,什么时候?是什么让这个答案不止于此 适度的论点? - Guildenstern


来自谷歌的Misko Hevery有一些关于这个话题的有趣文章......

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

单身人士只不过是全球化的国家。全局状态使得您的对象可以秘密地掌握未在其API中声明的内容,因此,单身人士会将您的API变成病态的骗子。

所有单身人士都去了哪里 表明依赖注入使得向需要它们的构造函数获取实例变得容易,这减轻了第一篇文章中谴责的糟糕的全局Singletons背后的潜在需求。


200



Misko关于这个主题的文章是目前最好的文章。 - Chris Mayer
第一个链接实际上并没有解决单例的问题,而是在类中假设一个静态依赖。可以通过传入参数来修复给定的示例,但仍然使用单例。 - DGM
@DGM:确切地说 - 事实上,文章的“基本原理”部分与“单身人士是原因”部分之间存在巨大的逻辑脱节。 - Harper Shelby
Misko的CreditCard文章是一篇 极端 滥用这种模式的例子。 - Alex
阅读第二篇文章单身实际上是所描述的对象模型的一部分。但是,它们不是全局可访问的,而是Factory对象的私有。 - FruitBreak


我认为混淆是由于人们不知道Singleton模式的真实应用。我不能强调这一点。辛格尔顿是  包装全局变量的模式。单例模式只应用于保证 给定类的唯一一个实例 在运行时存在。

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


103



什么是一个实例,可在整个应用程序中使用?哦。 全球。 “单身人士是 不 包装全局变量的模式“要么天真,要么误导,如同 根据定义,该模式围绕全局实例包装一个类。 - cHao
当然,您可以在应用程序启动时自由创建该类的一个实例,并通过接口将该实例注入到使用它的任何内容中。实施不应该关心只有一个。 - Scott Whitlock
@Dainius:最后真的没有。当然,你不能随意用另一个替换实例。 (除了那些引人注目的时刻 你做。我真的见过 setInstance 之前的方法。)但这几乎不重要 - 那个“需要”单身人士的weenie也不知道封装的可怕事情或可变全局状态有什么问题,所以他帮助(?)为每个人提供了setter。单。领域。 (是的,这发生了.A 批量。几乎我在野外见过的每一个单身都是可变的设计,而且经常令人尴尬。) - cHao
@Dainius:在很大程度上,我们已经有了。 “优先考虑构成而不是继承”已经有一段时间了。什么时候继承 是 但是,你可以自由地使用它,这是最好的解决方案。单身,全局,线程, goto等等他们可能 工作 在许多情况下,但坦率地说,“作品”是不够的 - 如果你想违背传统智慧,你最好能够证明你的方法是什么 更好 比传统的解决方案。我还没有看到Singleton模式的这种情况。 - cHao
为了避免我们互相讨论,我不仅仅是谈论全球可用的实例。有很多案例可以解决这个问题。我正在谈论的内容(特别是当我说“Capital-S Singleton”)是GoF的Singleton模式,它将单个全局实例嵌入到类本身中,通过 getInstance 或类似命名的方法,并防止存在第二个实例。坦率地说,在那一点上,你甚至可能没有 一 实例。 - cHao


关于单身人士的一个相当不好的事情是你不能很容易地扩展它们。你基本上必须建立某种形式 装饰图案 或者某些这样的事情,如果你想改变他们的行为。另外,如果有一天你想要有多种方法来做这件事,那么根据你的代码布局方式进行更改可能会非常痛苦。

有一点需要注意,如果你使用单身人士,试着将它们传递给任何需要它们的人,而不是让他们直接访问它们......否则,如果你选择有多种方式来完成单身人士所做的事情,那么它将是因为每个类在直接访问单例时嵌入依赖项,所以很难改变。

所以基本上:

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

而不是:

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

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

像任何模式一样......考虑一下并考虑它在特定情况下的使用是否不合适......规则通常会被破坏,并且 模式 如果没有想到,不应该毫无疑问地应用。


71



嘿,如果你到处都这样做,那么你也必须在各处传递对singelton的引用,因此你不再有单例。 :)(这通常是IMO的一件好事。) - Bjarke Freund-Hansen
@BjarkeFreund-Hansen - 胡说八道,你在说什么?单例只是一次实例化的类的实例。引用这样的Singleton不会复制实际对象,它只是引用它 - 你仍然有相同的对象(读取:Singleton)。 - M. Mimpen
@ M.Mimpen:不,资本-S Singleton(这里正在讨论的是)一个类的实例(a) 担保 只有一个实例将存在,并且(b)通过类自己的内置全局访问点访问。如果你宣布什么都不应该打电话 getInstance()那么(b)就不再那么真实了。 - cHao
@cHao我不跟你或你不明白我评论谁 - 这是Bjarke Freund-Hansen。 Bjarke说,有几个单身人士参考结果有几个单身人士。这当然不是真的,因为没有深拷贝。 - M. Mimpen
@ M.Mimpen:我的评论更多地引用了语义效应。一旦你取消了电话 getInstance(),你已经有效地抛出了Singleton模式和普通引用之间的一个有用的区别。就其余代码而言,单一性不再是属性。只有来电者 getInstance() 需要知道甚至关心有多少个实例。如果只有一个调用者,那么为了可靠地强制执行单一性而不是让调用者只存储引用并重用它,它在成本和灵活性方面的成本更高。 - cHao


单身模式本身不是问题。问题在于,人们经常使用这种模式来开发具有面向对象工具的软件,而没有扎实地掌握OO概念。当在这种情况下引入单例时,它们往往会成长为无法管理的类,每个小用途都包含辅助方法。

从测试的角度来看,单身人士也是一个问题。他们倾向于使孤立的单元测试难以编写。 控制倒置 (IoC)和 依赖注入 是一种旨在以面向对象的方式克服这个问题的模式,它有助于单元测试。

在一个 垃圾收集 环境单例很快就会成为内存管理方面的问题。

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


65



我知道这是多年的线程。嗨@Kimoz你说: - 单身人士很快就会成为内存管理方面的问题。我想更详细地解释一下单身和垃圾收集会出现什么样的问题。 - Thomas
@Kimoz,问题是问 “为什么单身人士模式本身不是问题?” 你只是重复了这一点,但没有提供单一模式的有效用例。 - Pacerier
@Thomas,因为根据定义,单例只存在于一个实例中。因此,将唯一引用赋予null通常很复杂。它可以完成,但这意味着您可以完全控制应用程序中未使用单例的点。这种情况很少见,而且通常与单身爱好者正在寻找的情况完全相反:制作单个实例的简单方法 总是 无障碍。在一些像Guice或Dagger这样的DI系列中,不可能摆脱单身,它永远留在记忆中。 (虽然提供单件的集装箱远比自制单件好得多)。 - Snicolas


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


52



这听起来更像是可以测试对象(单元),函数(单元),整个库(单元)的单元测试的问题,但是在类(也是单元)中有任何静态的失败。 - v010dya
你不是想要所有外部参考吗?如果你是,那么moc singleton有什么问题,如果没有,你真的在​​做单元测试吗? - Dainius
@Dainius:嘲笑 实例 比嘲弄课程要麻烦得多。你可以想象从应用程序的其余部分中提取被测试的类并用假测试 Singleton 类。然而,这极大地使测试过程复杂化。首先,现在你需要能够随意卸载类(在大多数语言中不是真正的选项),或者为每个测试启动一个新的VM(读取:测试可能需要数千倍的时间)。但是对于两个,依赖于 Singleton 是一个实现细节,现在正在您的测试中泄漏。 - cHao
Powermock可以模拟静态的东西。 - Snicolas


单身人士也很糟糕 集群。因为那时,你的应用程序中不再有“完全一个单独”。

请考虑以下情况:作为开发人员,您必须创建一个访问数据库的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

这听起来很奇怪,但是 现在你的申请中有很多单身人士。而这正是单身人士不应该做的事情:有很多对象。如果您(如此示例中所示)想要对数据库进行同步调用,则这尤其糟糕。

当然,这是单身人士使用不当的一个例子。但是这个例子的信息是:你不能依赖应用程序中只有一个单例实例 - 特别是在集群方面。


42



如果您不知道如何实现单例,则不应该这样做。如果你不知道自己在做什么,你应该先找到它,然后才能找到你需要的东西。 - Dainius
这很有趣,我有一个问题。那么,如果单身人士(每个人在不同的机器/ JVM上)连接到一个数据库,究竟会出现什么问题呢?单例范围仅适用于特定的JVM,即使在集群中也是如此。而不仅仅是哲学上说这种特殊情况是坏的,因为我们的意图是跨应用程序的单个对象,我很乐意看到由于这种安排可能出现的任何技术问题。 - Saurabh Patil