该单例模式是一个缴足成员四人帮的模式书,但最近似乎而是由开发者世界孤立.我仍然使用相当多的单例,特别是对于工厂类,虽然你必须对多线程问题(实际上是任何类)有点小心,但我不明白为什么它们如此可怕.
Stack Overflow特别假设每个人都同意Singletons是邪恶的.为什么?
请用" 事实,参考或特定专业知识 " 支持您的答案
从Brian Button转述:
它们通常用作全局实例,为什么这么糟糕?因为您在代码中隐藏了应用程序的依赖关系,而不是通过接口公开它们.制作一些全局的东西以避免传递它是一种代码味道.
他们违反了单一责任原则:由于他们控制着自己的创造和生命周期.
它们固有地导致代码紧密耦合.在许多情况下,这使得将它们伪装在测试中相当困难.
它们在应用程序的生命周期中处于状态.测试的另一个打击因为你可能最终会遇到需要订购测试的情况,这对于单元测试来说是一个很大的问题.为什么?因为每个单元测试应该独立于另一个.
单身人士解决了一个(也是唯一一个)问题.
资源争用.
如果你有一些资源
(1)只能有一个实例,而且
(2)您需要管理该单个实例,
你需要一个单身人士.
没有很多例子.日志文件是最重要的.您不想只放弃单个日志文件.您想要正确刷新,同步和关闭它.这是必须管理的单个共享资源的示例.
你很少需要一个单身人士.他们不好的原因是他们觉得自己是全球性的,他们是GoF 设计模式书的全额付费会员.
当你认为自己需要全局时,你可能会犯一个可怕的设计错误.
一些编码势利者瞧不起它们只是一个美化的全球化.就像许多人讨厌goto声明一样,还有其他人讨厌使用全局的想法.我见过几个开发人员为了避免全球化而竭尽全力,因为他们考虑使用一个作为承认失败.奇怪但真实.
在实践中,Singleton模式只是一种编程技术,是您的概念工具包的有用部分.您可能会不时发现它是理想的解决方案,因此请使用它.但是使用它只是为了让你夸耀使用设计模式就像拒绝使用它一样愚蠢,因为它只是一个全球性的.
来自谷歌的Misko Hevery有一些关于这个主题的有趣文章......
单身人士是Pathological Liars有一个单元测试示例,说明了单身人士如何难以找出依赖链并开始或测试应用程序.这是滥用的一个相当极端的例子,但他提出的观点仍然有效:
单身人士只不过是全球化的国家.全局状态使得您的对象可以秘密地获取未在其API中声明的内容,因此,单身人士会将您的API变成病态的骗子.
所有Singletons Gone都指出依赖注入使得向需要它们的构造函数获取实例变得容易,这减轻了第一篇文章中谴责的糟糕的全局Singletons背后的潜在需求.
我认为混淆是由于人们不知道Singleton模式的真实应用.我不能强调这一点.Singleton 不是一个包装全局变量的模式.单例模式应该仅用于保证在运行时期间存在给定类的一个且仅一个实例.
人们认为Singleton是邪恶的,因为他们将它用于全局变量.正是由于这种混乱,单身人士被人瞧不起.请不要混淆Singletons和全局.如果用于它的目的,您将从Singleton模式中获得极大的好处.
关于单身人士的一个相当不好的事情是你不能很容易地扩展它们.如果你想改变他们的行为,你基本上必须建立某种装饰模式或某些东西.另外,如果有一天你想要有多种方法来做这件事,那么改变可能会非常痛苦,这取决于你如何布置你的代码.
有一点需要注意,如果你使用单身人士,试着把它们传递给任何需要它们的人,而不是让他们直接访问它......否则,如果你选择有多种方式来完成单身人士所做的事情,那么它将是因为每个类在直接访问单例时嵌入依赖项,所以很难改变.
所以基本上:
public MyConstructor(Singleton singleton) {
this.singleton = singleton;
}
而不是:
public MyConstructor() {
this.singleton = Singleton.getInstance();
}
我相信这种模式称为依赖注入,通常被认为是一件好事.
像任何模式一样......考虑一下并考虑它在给定情况下的使用是否不合适......规则通常会被破坏,并且不应该毫无思想地应用模式.
单身模式本身不是问题.问题在于,人们经常使用这种模式来开发具有面向对象工具的软件,而没有扎实地掌握OO概念.当在这种情况下引入单例时,它们往往会成长为无法管理的类,每个小用途都包含辅助方法.
从测试的角度来看,单身人士也是一个问题.他们倾向于使孤立的单元测试难以编写.控制反转(IoC)和依赖注入是一种模式,旨在以面向对象的方式克服这个问题,这有助于单元测试.
在垃圾收集环境中,单例很快就会成为内存管理方面的问题.
还存在多线程场景,其中单例可能成为瓶颈以及同步问题.
使用静态方法实现单例.进行单元测试的人员可以避免静态方法,因为他们不能被模拟或存根.该网站上的大多数人都是单元测试的重要支持者.通常最接受的避免它们的惯例是使用控制模式的反转.
在集群方面,单身人士也很糟糕.因为那样,你的应用程序中就没有"完全一个单独"了.
请考虑以下情况:作为开发人员,您必须创建一个访问数据库的Web应用程序.要确保并发数据库调用不会相互冲突,请创建线程保存SingletonDao
:
public class SingletonDao { // songleton's static variable and getInstance() method etc. omitted public void writeXYZ(...){ synchronized(...){ // some database writing operations... } } }
因此,您确定应用程序中只存在一个单例,并且所有数据库都只通过此单例SingletonDao
.您的生产环境现在看起来像这样:
到目前为止一切都很好.
现在,考虑您要在群集中设置Web应用程序的多个实例.现在,你突然间有这样的事情:
这听起来很奇怪,但现在你的申请中有很多单身人士.而这正是单身人士不应该做的事情:有很多对象.如果您(如此示例中所示)想要对数据库进行同步调用,则这尤其糟糕.
当然,这是单身人士使用不当的一个例子.但是这个例子的信息是:你不能依赖应用程序中只有一个单例实例 - 特别是在集群方面.
它很容易(ab)用作全局变量.
依赖于单例的类相对更难以单独进行单元测试.
垄断是魔鬼,而非只读/可变状态的单身人士是"真正的"问题......
看完后单身人士病态说谎者在建议贾森的回答我碰到这个小珍闻,提供最佳的呈现例子就是如何单身常常被误用.
全球是坏的,因为:
一个.它导致命名空间冲突
湾 它以无根据的方式暴露了国家
说到单身人士
一个.调用它们的显式OO方式可以防止冲突,所以指向a.不是问题
湾 没有国家的单身人士(像工厂)不是问题.具有状态的单身人士可以再次分为两类,一类是不可变的或一次写入并且读取很多(配置/属性文件).这些都不错.作为参考持有者的可变单身人士就是你所说的那些人.
在最后一个声明中,他指的是博客的"单身人士是骗子"的概念.
这如何适用于垄断?
要开始一场垄断游戏,首先:
我们首先建立规则,以便每个人都在同一页面上
在比赛开始时,每个人都有一个平等的开始
只提出一套规则以避免混淆
规则不允许在整个游戏中改变
现在,对于任何没有真正垄断的人来说,这些标准至多是理想的.垄断的失败是难以接受的,因为垄断是关于金钱的,如果你输了,你必须煞费苦心地观察剩下的球员完成比赛,而且损失通常是迅速和惨淡的.因此,规则通常会在某些时候扭曲,以牺牲其他人的利益为一些球员的自身利益服务.
所以你和朋友Bob,Joe和Ed一起垄断.您正迅速建立自己的帝国,并以指数的速度消耗市场份额.你的对手正在减弱,你开始嗅到血(比喻).你的好友鲍勃把他所有的钱都投入到尽可能多的低价值房产中,但他没有像他预期的那样获得高额投资回报.鲍勃,作为一个运气不好的中风,落在你的木板路上,并从游戏中被切除.
现在游戏从友好的骰子滚动到严肃的事业.鲍勃已经成为失败的榜样,乔和埃德不想最终像'那个家伙'.所以,作为领先的玩家,你突然变成了敌人.乔和埃德开始练习桌下交易,背后的钱注射,低估的房屋交换以及一般会削弱你作为一名球员的任何东西,直到其中一人升到顶峰.
然后,不是其中一个获胜,而是从头开始.突然之间,一组有限的规则成为一个移动的目标,游戏退化为社交互动的类型,构成了自幸存者以来每一个高评级真人秀节目的基础.为什么,因为规则正在发生变化,并且没有就如何/为什么/它们应该代表什么达成共识,更重要的是,没有人做出决定.在那一点上,游戏中的每个玩家都在制定他/她自己的规则并且随后发生混乱,直到两个玩家太累而无法跟上游戏并慢慢放弃.
因此,如果游戏的规则手册准确地代表了单身人士,那么垄断规则手册将成为滥用的一个例子.
这如何适用于编程?
除了可变单例存在的所有明显的线程安全和同步问题之外......如果你有一组数据,它能够被多个不同的源同时读取/操作,并且在应用程序执行的生命周期中存在,这可能是退后一步并问"我在这里使用正确类型的数据结构"的好时机.
就个人而言,我看到程序员滥用单例,将其用作应用程序中的某种扭曲的跨线程数据库存储.直接处理代码后,我可以证明它很慢(因为所有线程锁都需要使其成为线程安全的)并且是一个噩梦(由于同步错误的不可预测/间歇性),以及几乎不可能在"生产"条件下进行测试.当然,可以使用轮询/信令来开发一个系统来克服一些性能问题但是这不能解决测试问题,并且为什么当"真正的"数据库已经能够以更加健壮的方式完成相同的功能时呢? /可扩展的方式.
单身是唯一的选择,如果你需要什么单提供.对象的write-one只读实例.同样的规则也应该级联到对象的属性/成员.
与其他答案不同,我不想谈谈单身人士出了什么问题,而是向你展示他们使用时的强大和真棒!
问题:Singleton在多线程环境中可能是一个挑战
解决方案:使用单线程引导过程来初始化单例的所有依赖关系.
问题:很难模仿单身人士.
解决方案:使用方法工厂模式进行模拟
MyModel
您可以映射TestMyModel
到MyModel
继承它的类,无论何时TestMyModel
注入,您都将获得MyModel
instread.
问题:单身人士可能因为他们从未处理过而导致记忆韭菜.
解决方案:好吧,处理它们!在您的应用程序中实现回调以正确处理单例,您应该删除链接到它们的任何数据,最后:从工厂中删除它们.
正如我在标题中所述,单身人士不是单身实例.
单身人士提高了可读性:你可以看看你的课程,看看它注入了什么单身人士来弄清楚它的依赖性.
单身人士改进了维护:一旦你从一个类中删除了一个你只删除了一些单例注入的依赖,你就不需要去编辑其他类的大链接了,这些类只是移动了你的依赖(这是我的臭代码@Jim Burger)
单身人士提高了内存和性能:当你的应用程序发生某些事情,并且需要一长串回调来传递时,你就会浪费内存和性能,通过使用Singleton你正在削减中间人,并提高你的性能和内存使用率(通过避免不必要的局部变量分配).
关于单身人士如何做坏的答案总是"他们很难做对".语言的许多基础组件都是单例(类,函数,命名空间甚至运算符),计算的其他方面(localhost,默认路由,虚拟文件系统等)中的组件也是如此,并且不是偶然的.虽然他们不时会引起麻烦和挫折,但他们也可以使很多事情变得更好.
我看到的两个最大的问题是:将它视为全局并且无法定义Singleton闭包.
每个人都把单身人士称为全局,因为他们基本上都是.然而,全球范围内的许多(可悲的是,并非全部)不良并非本质上来自于全球性,而是如何使用它.单身人士也是如此.实际上更多,因为"单一实例"实际上并不需要意味着"全球可访问".它更像是一种天然的副产品,考虑到我们所知道的所有坏事,我们不应该急于利用全球可访问性.一旦程序员看到Singleton,他们似乎总是通过其实例方法直接访问它.相反,您应该像对待任何其他对象一样导航到它.大多数代码甚至不应该意识到它正在处理Singleton(松散耦合,对吧?).如果只有一小部分代码访问对象,就像它是全局的一样,那么很多伤害都会被撤消.我建议通过限制对实例函数的访问来强制执行它.
Singleton语境也非常重要.Singleton的定义特征是"只有一个",但事实是它在某种上下文/命名空间中"只有一个".它们通常是以下之一:每个线程,进程,IP地址或集群一个,但也可以是每个处理器,机器,语言命名空间/类加载器/任何,子网,Internet等一个.
另一个不太常见的错误是忽略了Singleton的生活方式.仅仅因为只有一个并不意味着Singleton是一些无所不能的"永远是并且永远都会",也不是一般所希望的(没有开头和结尾的对象违反了代码中的各种有用的假设,并且只应该被使用在最绝望的情况下.
如果你避免这些错误,Singletons仍然可以成为一个PITA,它已经准备好看到很多最糟糕的问题都会大大减轻.想象一下Java Singleton,它被明确定义为每个类加载器一次(这意味着它需要一个线程安全策略),定义的创建和销毁方法以及指示何时以及如何调用它们的生命周期,以及其"实例"方法具有的包保护,因此通常通过其他非全局对象访问.仍然是潜在的麻烦来源,但肯定要少得多麻烦.
可悲的是,而不是教导如何做单身人士的好例子.我们教导不好的例子,让程序员暂时使用它们,然后告诉他们这是一个糟糕的设计模式.
请参阅Wikipedia Singleton_pattern
它也被一些人认为是一种反模式,他们认为它被过度使用,在实际上不需要类的唯一实例的情况下引入了不必要的限制.[1] [2] [3] [4]
参考文献(仅文章中的相关参考文献)
^亚历克斯米勒.模式我讨厌#1:Singleton,2007年7月
^斯科特Densmore.为什么单身人士是邪恶的,2004年5月
^ Steve Yegge.2004年9月,单身人士认为这是愚蠢的
^ JB Rainsberger,IBM.2001年7月明智地使用你的单身人士
不是单身人士本身不好,而是GoF的设计模式.唯一真正有效的论点是GoF设计模式在测试方面不适用,特别是如果测试是并行运行的话.
只要在代码中应用以下方法,使用类的单个实例就是有效的构造:
确保将用作单例的类实现接口.这允许使用相同的接口实现存根或模拟
确保Singleton是线程安全的.这是给定的.
单身人员应该是简单的,而不是过于复杂.
在应用程序的运行时期间,需要将单例传递给给定对象,使用构建该对象的类工厂并让类工厂将单例实例传递给需要它的类.
在测试期间并确保确定性行为,将单例类创建为单独的实例,作为实际类本身或实现其行为的存根/模拟,并将其原样传递给需要它的类.不要使用在测试期间创建需要单例的测试对象的类因子,因为它会传递它的单个全局实例,这会破坏目的.
我们在我们的解决方案中使用了Singletons,取得了巨大成功,可以测试并行测试运行流中的确定性行为.
我想在接受的答案中提出4点,希望有人可以解释为什么我错了.
为什么隐藏代码中的依赖项不好?已经存在许多隐藏的依赖项(C运行时调用,OS API调用,全局函数调用),并且易于查找单例依赖项(搜索instance()).
"让一些东西变得全球化以避免传递它是一种代码味道." 为什么不传递一些东西以避免使它成为代码气味的单身?
如果你通过调用堆栈中的10个函数传递一个对象只是为了避免单例,那么这么棒吗?
单一责任原则:我认为这有点模糊,取决于您对责任的定义.一个相关的问题是,为什么在课堂上增加这种具体的 "责任"呢?
为什么将一个对象传递给一个类使它比使用该对象作为一个单独的类更紧密耦合?
为什么它会改变国家持续多久?单例可以手动创建或销毁,因此控件仍然存在,并且您可以使生命周期与非单例对象的生命周期相同.
关于单元测试:
并非所有课程都需要进行单元测试
并非所有需要进行单元测试的类都需要更改单例的实现
如果他们确实需要进行单元测试并且确实需要更改实现,那么很容易将类从使用单例改为通过依赖注入将单例传递给它.
Vince Huston有这些标准,这对我来说似乎是合理的:
只有满足以下所有三个标准时,才应考虑单身人士:
无法合理分配单个实例的所有权
延迟初始化是可取的
不提供全球访问权限
如果单个实例的所有权,初始化发生的时间和方式以及全局访问都不是问题,那么Singleton就不够有趣了.
从纯粹主义的观点来看,单身人士是坏人.
从实际的角度来看,单身是一种权衡发展时间与复杂性的关系.
如果你知道你的应用程序不会改变那么多,那么它们就可以了.只要知道如果你的需求以一种意想不到的方式发生变化,你可能需要重构一下(在大多数情况下这是非常好的).
单身人士有时也会使单元测试复杂化.
模式没有任何内在错误,假设它被用于模型的某些方面,这是真正单一的.
我认为反弹是由于过度使用,反过来,这是由于它是最容易理解和实施的模式.
我不打算评论好/坏的论点,但是自从Spring出现以来我没有使用它们.使用依赖注入几乎消除了我对单例,服务定位器和工厂的要求.我发现这是一个更高效,更干净的环境,至少对于我所做的工作类型(基于Java的Web应用程序).
Singleton是一种模式,可以像任何其他工具一样使用或滥用.
单身人员的坏部分通常是用户(或者我应该说不适当地使用单身人士来做它不打算做的事情).最大的罪犯使用单身人士作为虚假的全球变量.
当您使用单例编写代码时,例如记录器或数据库连接,然后您发现需要多个日志或多个数据库,您就遇到了麻烦.
单身人士很难从他们移动到常规物体.
此外,编写非线程安全的单例太容易了.
您应该将所有必需的实用程序对象从函数传递给函数,而不是使用单例.如果将它们全部包装到辅助对象中,可以简化这一点,如下所示:
void some_class::some_function(parameters, service_provider& srv) { srv.get().log("Hi there!"); this->another_function(some_other_parameters, srv); }
最近关于这个主题的文章由Chris Reath编写,没有评论.
注意:没有注释的编码不再有效.但是,链接到的文章已被其他用户克隆.
http://geekswithblogs.net/AngelEyes/archive/2013/09/08/singleton-i-love-you-but-youre-bringing-me-down-re-uploaded.aspx
单身人士的问题是增加范围和因此耦合的问题.无可否认,在某些情况下,您确实需要访问单个实例,并且可以通过其他方式完成.
我现在更喜欢围绕控制反转(IoC)容器进行设计,并允许容器控制生命周期.这使得依赖于实例的类的好处是不知道存在单个实例的事实.单身人士的生命周期可以在未来改变.一旦我最近遇到的这样的例子是从单线程到多线程的简单调整.
FWIW,如果它是PIA,当你尝试对它进行单元测试时,那么当你尝试调试,修复bug或增强它时,它会进入PIA.
单身人士并不坏.只有当你创造出全球唯一并且不是全球唯一的东西时,这才是最糟糕的.
但是,有"应用程序范围服务"(考虑使组件交互的消息传递系统) - 这个CALLS用于单例,"MessageQueue" - 类具有方法"SendMessage(...)".
然后,您可以从所有地方执行以下操作:
MessageQueue.Current.SendMessage(new MailArrivedMessage(...));
当然,这样做:
MessageQueue.Current.RegisterReceiver(本);
在实现IMessageReceiver的类中.
太多人将单个模式中非线程安全的对象放置.我已经看到了以单例模式完成的DataContext(LINQ to SQL)的例子,尽管DataContext不是线程安全的,而且纯粹是一个工作单元对象.
关于单身人士还有一件事,没有人说过.
在大多数情况下,"单一性"是某些类的实现细节,而不是其接口的特征.控制容器的反转可以隐藏类用户的这种特征; 你只需要将你的类标记为单例(@Singleton
例如在Java中使用注释),就是这样; IoCC将完成剩下的工作.您不需要提供对单例实例的全局访问权限,因为访问权限已由IoCC管理.因此,IoC Singletons没有任何问题.
与IoC Singletons相反的GoF Singletons应该通过getInstance()方法在界面中暴露"单一性",这样他们就会遭遇上述所有内容.
如果您正确且最少地使用它,那么单例并不是邪恶的。还有许多其他好的设计模式可以在某个时候替代单例的需求(并且也可以提供最佳结果)。但是一些程序员没有意识到那些好的模式,并在所有情况下都使用单例,这使单例对他们而言是邪恶的。