singleton pattern是GoF patterns book的完全付费会员,但最近似乎是开发者世界的孤儿。我仍然使用了很多单身人士,特别是对于factory classes,虽然你必须对多线程问题(实际上是任何类)有点小心,但我不明白为什么他们这么糟糕。
堆栈溢出特别假设每个人都认为单身人士是邪恶的。为什么呢?请使用“事实,参考资料或特定专业知识”来支持您的答案
答案 0 :(得分:1226)
布莱恩·巴顿解释:
它们通常用作全局实例,为什么这么糟糕?因为您在代码中隐藏了应用程序的依赖关系,而不是通过接口公开它们。制作一些全局的东西以避免传递它是code smell。
他们违反single responsibility principle:因为他们控制着自己的创造和生命周期。
它们固有地导致代码紧密coupled。在许多情况下,这使得将它们伪装在测试中相当困难。
它们在应用程序的生命周期中携带状态。测试的另一个打击因为你可能最终会遇到需要订购测试的情况,这对于单元测试来说是一个很大的问题。为什么?因为每个单元测试应该独立于另一个。
答案 1 :(得分:426)
单身人士解决了一个(也是唯一一个)问题。
资源争用。
如果你有一些资源
( 1 )只能有一个实例,
( 2 )您需要管理该单个实例
您需要单身。
没有很多例子。日志文件是最重要的。您不想只放弃单个日志文件。您想要正确刷新,同步和关闭它。这是必须管理的单个共享资源的示例。
你很少需要一个单身人士。他们不好的原因是他们觉得自己是global,而且他们是GoF Design Patterns 一书的完全付费会员。
当你认为自己需要全局时,你可能会犯一个糟糕的设计错误。
答案 2 :(得分:325)
一些编码势利者瞧不起他们只是一个美化的全球化。就像许多人讨厌 goto 语句一样,还有其他人讨厌使用全局的想法。我见过几个开发人员为避免全局而花费了不少时间,因为他们考虑使用一个来承认失败。奇怪但真实。
在实践中, Singleton 模式只是一种编程技术,是您的概念工具包的有用部分。您可能会不时发现它是理想的解决方案,因此请使用它。但是使用它只是为了让你自夸使用设计模式就像拒绝使用它一样愚蠢,因为它只是一个全局。
答案 3 :(得分:212)
Singletons are Pathological Liars有一个单元测试示例,说明了单例如何使得很难找出依赖链并启动或测试应用程序。这是滥用的一个相当极端的例子,但他提出的观点仍然有效:
单身人士只不过是全球化的国家。全局状态使得您的对象可以秘密地掌握未在其API中声明的内容,因此,单身人士会将您的API变成病态的骗子。
Where have all the Singletons Gone指出依赖注入使得向需要它们的构造函数获取实例变得容易,这减轻了第一篇文章中谴责的糟糕的全局Singletons背后的潜在需求。
答案 4 :(得分:112)
我认为混淆是由于人们不知道Singleton模式的真实应用这一事实。我不能强调这一点。 Singleton 不包装全局变量的模式。单例模式应该仅用于保证在运行时期间存在一个且只有一个给定类的实例。
人们认为Singleton是邪恶的,因为他们将它用于全局变量。正是由于这种混乱,单身人士被人瞧不起。请不要混淆Singletons和全局。如果用于它的目的,您将从Singleton模式中获得极大的好处。
答案 5 :(得分:71)
单身人士的一个相当不好的地方是你不能轻易地扩展它们。如果你想改变他们的行为,你基本上必须构建某种decorator pattern或某种类似的东西。此外,如果有一天你想要有多种方法来做这件事,那么改变可能会非常痛苦,这取决于你如何布置你的代码。
有一点需要注意,如果你使用单身人士,试着将它们传递给任何需要它们的人,而不是让他们直接访问它......否则,如果你选择有多种方式来完成单身人士所做的事情,如果每个类在直接访问单例时嵌入依赖项,那么更改将会非常困难。
基本上是这样的:
public MyConstructor(Singleton singleton) {
this.singleton = singleton;
}
而不是:
public MyConstructor() {
this.singleton = Singleton.getInstance();
}
我相信这种模式被称为dependency injection,通常被认为是一件好事。
与任何模式一样......考虑一下并考虑它在特定情况下的使用是否不合适......规则通常会被破坏,patterns不应该在没有规则的情况下应用思想。
答案 6 :(得分:67)
单身模式本身不是问题。问题在于,人们经常使用这种模式来开发具有面向对象工具的软件,而没有扎实地掌握OO概念。当在这种情况下引入单例时,它们往往会成长为无法管理的类,每个小用途都包含辅助方法。
从测试的角度来看,单身人士也是一个问题。他们倾向于使孤立的单元测试难以编写。 Inversion of control (IoC)和 dependency injection 是用于以面向对象的方式克服此问题的模式,适合单元测试。
在garbage collected环境中,单身人士很快就会成为内存管理方面的问题。
还存在多线程场景,单身人士可能成为瓶颈以及同步问题。
答案 7 :(得分:53)
使用静态方法实现单例。进行单元测试的人员可以避免使用静态方法,因为它们不能被模拟或存根。该网站上的大多数人都是单元测试的重要支持者。通常最常用的避免它们的惯例是使用inversion of control模式。
答案 8 :(得分:44)
单身人士在群集方面也很糟糕。因为那样,你的应用程序中就没有“完全一个单独”了。
请考虑以下情况:作为开发人员,您必须创建一个访问数据库的Web应用程序。要确保并发数据库调用不会相互冲突,可以创建线程保存SingletonDao
:
public class SingletonDao {
// songleton's static variable and getInstance() method etc. omitted
public void writeXYZ(...){
synchronized(...){
// some database writing operations...
}
}
}
因此,您确定应用程序中只存在一个单例,并且所有数据库都只通过此SingletonDao
。您的生产环境现在看起来像这样:
到目前为止,一切都很好。
现在,考虑您要在群集中设置Web应用程序的多个实例。现在,你突然间有这样的事情:
这听起来很奇怪,但现在你的应用程序中有很多单身人士。而这正是单身人士不应该做的事情:有很多对象。如果您(如本示例所示)想要对数据库进行同步调用,那么这尤其糟糕。
当然,这是单身人士使用不当的一个例子。但是这个例子的信息是:你不能依赖应用程序中只有一个单例实例 - 特别是在集群方面。
答案 9 :(得分:38)
答案 10 :(得分:30)
垄断是魔鬼,非读/可变状态的单身人士是'真正的'问题......
按照Singletons are Pathological Liars中的建议阅读jason's answer之后,我发现了这个小故事,它提供了如何单身人士经常被滥用的最佳例子。
全球是坏事,因为:
- 一个。它导致命名空间冲突
- 湾它以无根据的方式暴露国家
谈到单身人士
- 一个。调用它们的显式OO方式可以防止冲突,所以指向a。不是问题
- 湾没有国家的单身人士(像工厂)不是问题。具有状态的单身人士可以再次分为两类,一类是不可变的或一次写入并且读取很多(配置/属性文件)。这些都不错。作为参考持有者的可变单身人士就是你所说的。
在最后一句话中,他指的是博客的“单身人士是骗子”的概念。
这如何适用于垄断?
首先要开始一场垄断游戏:
现在,对于那些没有真正垄断的人来说,这些标准至多是理想的。垄断的失败是难以接受的,因为垄断是关于金钱的,如果你输了,你必须煞费苦心地观察剩下的球员完成比赛,而且损失通常是迅速和惨淡的。因此,规则通常会在某些时候扭曲,以牺牲其他玩家的利益为自己的利益服务。
所以你和朋友Bob,Joe和Ed一起垄断。您正迅速建立自己的帝国,并以指数的速度消耗市场份额。你的对手正在减弱,你开始嗅到血(比喻)。你的好友鲍勃把他所有的钱都投入到尽可能多的低价值房产中,但他没有像他预期的那样获得高额投资回报。鲍勃,作为一个运气不好的中风,落在你的木板路上,并从游戏中被切除。
现在游戏从友好的骰子滚动到严肃的事业。鲍勃已经成为失败的榜样,乔和埃德不想最终像'那个家伙'。所以,作为领先的玩家,你突然变成了敌人。乔和埃德开始练习桌下交易,背后的钱注射,低估的房屋交换以及一般会削弱你作为一名球员的任何东西,直到其中一人升到顶峰。
然后,这个过程开始了,而不是其中一个获胜。突然之间,一组有限的规则成为一个移动的目标,游戏退化为社交互动的类型,构成了自幸存者以来每一个高评级真人秀节目的基础。为什么,因为规则正在发生变化,并且没有就如何/为什么/它们应该代表什么达成共识,更重要的是,没有人做出决定。在那一点上,游戏中的每个玩家都在制定他/她自己的规则并且随后发生混乱,直到其中两个玩家太累而无法跟上游戏并慢慢放弃。
因此,如果游戏规则手册准确地代表了单身人士,那么垄断规则手册就会成为滥用的一个例子。
这如何适用于编程?
除了可变单例存在的所有明显的线程安全和同步问题之外......如果你有一组数据,它能够被多个不同的源同时读取/操作,并且在生命周期中存在。应用程序执行,现在可能是退后一步并问“我在这里使用正确类型的数据结构”的好时机。
就我个人而言,我已经看到程序员滥用单例,将其用作应用程序中的某种扭曲的跨线程数据库存储。直接处理代码后,我可以证明它很慢(因为所有线程锁都需要使其成为线程安全的)并且是一个噩梦(由于同步错误的不可预测/间歇性),以及几乎不可能在“生产”条件下进行测试。当然,可以使用轮询/信令来开发一个系统来克服一些性能问题但是这不能解决测试问题,并且为什么当“真正的”数据库已经能够以更加健壮的方式完成相同的功能时呢? /可扩展的方式。
如果您需要单身人士提供的内容,单身人士仅一个选项。对象的write-one只读实例。同样的规则也应该级联到对象的属性/成员。
答案 11 :(得分:22)
与其他答案不同,我不想谈谈单身人士出了什么问题,而是向你展示使用它们时有多强大和棒极了!
MyModel myModel = Factory.inject(MyModel.class);
您可以将MyModel
映射到继承它的TestMyModel
类,在注入MyModel
时,您将获得TestMyModel
instread。正如我在标题中所述,单身人士并不是单身实物。
答案 12 :(得分:21)
关于单身人士如何做坏事的答案总是,“他们很难做对”。语言的许多基础组件都是单例(类,函数,命名空间甚至运算符),计算的其他方面(localhost,默认路由,虚拟文件系统等)中的组件也是如此,并且不是偶然的。虽然他们不时会引起麻烦和挫折,但他们也可以使很多事情变得更好。
我看到的最大的两个问题是:将它视为一个全球性的& amp;未能定义Singleton闭包。
每个人都将单身人士称为全局,因为他们基本上都是。然而,全球范围内的许多(可悲的是,并非全部)不良并非本质上来自于全球性,而是如何使用它。单身人士也是如此。实际上更多,因为“单一实例”实际上并不需要意味着“全球可访问”。它更像是一种天然的副产品,考虑到我们所知道的所有坏事,我们不应该急于利用全球可访问性。一旦程序员看到Singleton,他们似乎总是通过其实例方法直接访问它。相反,您应该像对待任何其他对象一样导航到它。大多数代码甚至不应该意识到它正在处理Singleton(松散耦合,对吧?)。如果只有一小部分代码访问对象,就像它是全局的一样,那么很多伤害都会被撤消。我建议通过限制对实例函数的访问来强制执行它。
Singleton语境也非常重要。 Singleton的定义特征是“只有一个”,但事实是它在某种上下文/命名空间中“只有一个”。它们通常是以下之一:每个线程,进程,IP地址或集群一个,但也可以是每个处理器,机器,语言命名空间/类加载器/无论什么,子网,Internet等等。
另一个不常见的错误是忽略了Singleton的生活方式。仅仅因为只有一个并不意味着Singleton是一些无所不能的“永远是并且永远都会”,也不是一般所希望的(没有开头和结尾的对象违反了代码中的各种有用的假设,并且只应该被使用在最绝望的情况下。
如果你避免这些错误,Singletons仍然可以成为PITA,它已经准备好看到很多最糟糕的问题得到了显着缓解。想象一下Java Singleton,它被明确定义为每个类加载器一次(这意味着它需要一个线程安全策略),定义的创建和销毁方法以及指示何时以及如何调用它们的生命周期,以及其“实例”方法具有的包保护,因此通常通过其他非全局对象访问。仍然是潜在的麻烦来源,但肯定要少得多。
可悲的是,而不是教好单身人士的好例子。我们教导不好的例子,让程序员暂时使用它们,然后告诉他们这是一个糟糕的设计模式。答案 13 :(得分:21)
See Wikipedia Singleton_pattern
一些人也认为它是一种反模式,他们觉得它被过度使用,在实际上不需要一个类的唯一实例的情况下引入了不必要的限制。[1] [2] [3] [ 4]
参考文献(仅文章中的相关参考文献)
答案 14 :(得分:16)
单身人士本身并不好,但GoF的设计模式却是如此。唯一真正有效的论点是GoF设计模式在测试方面不适合,特别是如果测试是并行运行的。
只要在代码中应用以下方法,使用类的单个实例就是有效的构造:
确保将用作单例的类实现接口。这允许使用相同的接口
确保Singleton是线程安全的。这是给定的。
单身人士本质上应该简单,不要过于复杂。
在应用程序的运行时期间,需要将单例传递给给定对象,使用构建该对象的类工厂并让类工厂将单例实例传递给需要它的类。
在测试期间并确保确定性行为,将单例类创建为单独的实例,作为实际类本身或实现其行为的存根/模拟,并将其原样传递给需要它的类。不要使用在测试期间创建需要单例的测试对象的类因子,因为它会传递它的单个全局实例,这会破坏目的。
我们在我们的解决方案中使用了Singletons,取得了巨大的成功,可以确保并行测试运行流中的确定性行为。
答案 15 :(得分:15)
我想在接受的答案中提出4点,希望有人可以解释为什么我错了。
为什么隐藏代码中的依赖项不好?已经存在许多隐藏的依赖项(C运行时调用,OS API调用,全局函数调用),并且易于查找单例依赖项(搜索instance())。
“让一些东西变得全球化以避免传递它是一种代码味道。”为什么不传递一些东西以避免使它成为代码气味的单身?
如果你通过调用堆栈中的10个函数传递一个对象只是为了避免使用单例,那真是太棒了吗?
单一责任原则:我认为这有点模糊,取决于您对责任的定义。一个相关的问题是,为什么将这个具体的“责任”添加到一个类别的事情上呢?
为什么将一个对象传递给一个类使它比在该类中使用该对象作为一个单例更紧密耦合?
为什么它会改变国家持续多久?可以手动创建或销毁单身人士,因此控件仍然存在,并且您可以使生命周期与非单身对象的生命周期相同。
关于单元测试:
答案 16 :(得分:15)
Vince Huston有这些标准,对我来说似乎是合理的:
只有满足以下三个条件时才应考虑单身人士:
- 无法合理分配单个实例的所有权
- 延迟初始化是可取的
- 未另行规定全球访问权限
如果单个实例的所有权,初始化发生的时间和方式以及全局访问都不是问题,那么Singleton就不够有趣了。
答案 17 :(得分:13)
我不打算评论好/坏的论点,但是自Spring出现以来我没有使用它们。使用dependency injection几乎完全取消了我对单例,服务器和工厂的要求。我发现这是一个更高效,更干净的环境,至少对我的工作类型(基于Java的Web应用程序)而言。
答案 18 :(得分:12)
从纯粹主义的角度来看,单身人士是不好的。
从实际的角度来看,单身是一种权衡发展时间与复杂性的对比。
如果你知道你的应用程序不会改变那么多,那么它们就可以了。只要知道如果你的需求以一种意想不到的方式发生变化,你可能需要重构一下(在大多数情况下这是非常好的)。
单身人士有时也会使unit testing变得复杂。
答案 19 :(得分:12)
模式没有任何内在错误,假设它被用于模型的某些方面,这是真正单一的。
我认为反弹是由于过度使用,反过来,这是因为它是最容易理解和实施的模式。
答案 20 :(得分:11)
Singleton是一种模式,可以像任何其他工具一样使用或滥用。
单身人士的不良部分通常是用户(或者我应该说不适当地使用单身人士来做它不设计的事情)。最大的罪犯使用单身人士作为虚假的全球变量。
答案 21 :(得分:8)
使用单例编写代码时,例如记录器或数据库连接,然后发现需要多个日志或多个数据库,您就遇到了麻烦。
单身人士很难从他们转移到常规对象。
此外,编写非线程安全的单例也太容易了。
您应该将所有需要的实用程序对象从函数传递给函数,而不是使用单例。如果将它们全部包装到辅助对象中,可以简化这一点,如下所示:
void some_class::some_function(parameters, service_provider& srv)
{
srv.get<error_logger>().log("Hi there!");
this->another_function(some_other_parameters, srv);
}
答案 22 :(得分:7)
单身人士的问题是范围扩大的问题,因此coupling。无可否认,在某些情况下,您确实需要访问单个实例,并且可以通过其他方式完成。
我现在更喜欢围绕inversion of control(IoC)容器进行设计,并允许容器控制生命周期。这使得依赖于实例的类的好处是不知道存在单个实例的事实。单身人士的生命周期可以在未来改变。我最近遇到的这样一个例子是从单线程到多线程的简单调整。
FWIW,如果它是PIA,当你尝试进行单元测试时,那么当你尝试调试,错误修复或增强它时,它会进入PIA。
答案 23 :(得分:7)
Chris Reath在Coding Without Comments撰写的关于此主题的最新文章。
注意:没有评论的编码不再有效。但是,链接到的文章已被其他用户克隆。
答案 24 :(得分:6)
还有一个关于单身人士的事情还没有人说过。
在大多数情况下,“单一性”是某些类的实现细节,而不是其接口的特征。控制容器的反转可以隐藏类用户的这种特征;你只需要将你的类标记为单例(例如在Java中使用@Singleton
注释)就是这样; IoCC将完成剩下的工作。您不需要提供对单例实例的全局访问权限,因为访问权限已由IoCC管理。因此,IoC Singletons没有任何问题。
答案 25 :(得分:6)
单身人士并不坏。当你创造一种全球唯一的非全球唯一的东西时,这是唯一的不好。
但是,有“应用程序范围服务”(考虑使组件交互的消息传递系统) - 这个CALLS用于单例,“MessageQueue” - 具有“SendMessage(...)”方法的类。 / p>
然后,您可以从所有地方执行以下操作:
MessageQueue.Current.SendMessage(new MailArrivedMessage(...));
当然,这样做:
MessageQueue.Current.RegisterReceiver(本);
在实现IMessageReceiver的类中。
答案 26 :(得分:6)
太多人在单件模式中放置非线程安全的对象。我已经看到了以单例模式完成的DataContext(LINQ to SQL)的示例,尽管DataContext不是线程安全的,并且纯粹是一个工作单元对象。
答案 27 :(得分:4)
首先,一个班级及其合作者应该首先执行他们的预期目的,而不是专注于deoendents。生命周期管理(当实例在超出范围时被修饰)不应该成为工作人员责任的一部分。最常见的做法是使用依赖注入来创建或配置新组件来管理依赖关系。
软件通常变得越来越复杂,让“singleton”类的多个独立实例具有不同的状态是有意义的。在这种情况下,提交代码以简单地获取单例是错误的。使用Singleton.getInstance()对于小型简单系统可能没问题,但是当一个人可能需要同一个类的不同实例时,它不能工作/扩展。
任何类都不应该被认为是单例,而应该是它的用法或它如何用于配置依赖项的应用。对于快速和讨厌这没关系 - 只是卢克硬编码说文件路径无关紧要,但对于更大的应用程序,这些依赖关系需要考虑因素并使用DI以更合适的方式进行管理。
单身人士在测试中引起的问题是他们的硬编码单一用例/环境的症状。测试套件和许多测试都是单独的,并且与单独硬编码不兼容。
答案 28 :(得分:4)
因为它们基本上是面向对象的全局变量,所以通常可以这样设计类,以便不需要它们。
答案 29 :(得分:4)
Singletons 不是邪恶,如果你正确使用 &amp; 最低限度。还有很多其他优秀的设计模式可以在某些时候取代单身人士的需求(同时也能提供最好的结果)。但是一些程序员并不知道那些好的模式和对所有使单身人士变得邪恶的案件使用单身人士。
答案 30 :(得分:4)
当几个人(或团队)达到类似或相同的解决方案时,就会出现一种模式。很多人仍然使用原始形式的单身人士或使用工厂模板(在Alexandrescu的现代C ++设计中进行了很好的讨论)。管理对象生命周期的并发性和难度是主要障碍,前者可以按照您的建议轻松管理。
像所有选择一样,辛格尔顿有着相当大的起伏。我认为它们可以适度使用,特别是对于在应用程序生命周期内存活的对象。它们类似于(可能是)全局的事实可能会引发纯粹主义者。
答案 31 :(得分:3)
这是我认为到目前为止答案中缺少的内容:
如果每个进程地址空间需要一个此对象的实例(并且您对此要求不会有任何改变),则应将其设置为单例。
否则,它不是单身人士。
这是一个非常奇怪的要求,对用户来说几乎没有兴趣。进程和地址空间隔离是一个实现细节。它们仅在用户希望使用kill
或任务管理器停止您的应用程序时影响用户。
除了构建一个缓存系统之外,没有太多理由可以解释为什么每个进程只应该有一些实例。日志系统怎么样?可能更好的是每线程或更细粒度,以便您可以更自动地跟踪消息的来源。应用程序的主窗口怎么样?这取决于;也许你会希望所有用户的文档由于某种原因由同一个进程管理,在这种情况下,该进程中会有多个“主窗口”。
答案 32 :(得分:3)
作者提出的一些论点:
如果你以后需要让这个课程不单身,你就会陷入困境 完全没有 - 我在这种情况下使用单个数据库连接单例,我想变成一个连接池。 请记住,每个单例都是通过标准方法访问的:
MyClass.instance
这类似于工厂方法的签名。我所做的只是更新实例方法以返回池中的下一个连接 - 无需其他更改。如果我们没有使用单身人士,那就更难了。
单身人士只是花哨的全局 不能与之争论,但所有静态字段和方法都是如此 - 从类而不是实例访问的任何东西本质上都是全局的,我不会看到静态字段的使用有这么多的阻碍?
并不是说单身人士很好,只是在这里推翻一些“传统智慧”。
答案 33 :(得分:3)
Mark Radford的Singleton – the anti-pattern!(Overload Journal#57 - 2003年10月)很好地解读了为什么Singleton被认为是一种反模式。本文还讨论了替换Singleton的两种替代设计方法。
答案 34 :(得分:2)
假设您有一个单身人士,您可以从班级内的任何地方调用此实例。你的班级不再像以前那样纯洁。您的班级现在将不再对其成员及其明确接收的成员进行操作。这会产生混淆,因为类的用户不知道该类需要的足够信息是什么。 encapsulation的整个想法是隐藏用户的方法,但如果在方法中使用单例,则必须知道单例的状态才能正确使用该方法。这是反 - OOP。
答案 35 :(得分:1)
脱离我的头顶: