该单例模式是一个缴足成员四人帮的模式书,但最近似乎而是由开发者世界孤立。我仍然使用很多单例,尤其是对于工厂类,尽管您必须对多线程问题(实际上是任何类)有所注意,但我看不出它们为什么如此糟糕。
堆栈溢出似乎特别假设每个人都同意Singletons是邪恶的。为什么?
请以“ 事实,参考或特定专业知识 ” 支持您的回答
该单例模式是一个缴足成员四人帮的模式书,但最近似乎而是由开发者世界孤立。我仍然使用很多单例,尤其是对于工厂类,尽管您必须对多线程问题(实际上是任何类)有所注意,但我看不出它们为什么如此糟糕。
堆栈溢出似乎特别假设每个人都同意Singletons是邪恶的。为什么?
请以“ 事实,参考或特定专业知识 ” 支持您的回答
Answers:
布赖恩·巴顿(Brian Button)的释义:
单例解决一个(并且只有一个)问题。
资源争用。
如果您有一些资源
(1)只能有一个实例,并且
(2)您需要管理单个实例,
您需要一个单身人士。
例子不多。日志文件很大。您不想只放弃一个日志文件。您要正确刷新,同步并关闭它。这是必须管理的单个共享资源的示例。
您很少需要单身人士。他们之所以不好,是因为他们感觉自己像个全球人,并且是GoF Design Patterns书中的全薪会员。
当您认为需要全局性时,您可能会犯下可怕的设计错误。
一些编码小工具视它们为荣耀的全球对象。与许多人讨厌goto语句一样,其他人也讨厌曾经使用global的想法。我已经看到几个开发人员竭尽全力避免全局,因为他们考虑将开发人员视为失败的承认。奇怪但真实。
实际上,Singleton模式只是一种编程技术,它是概念工具包的有用部分。您可能会不时发现它是理想的解决方案,因此请使用它。但是使用它只是为了夸耀使用设计模式就像拒绝使用它一样愚蠢,因为它只是全局的。
来自Google的Misko Hevery就此主题发表了一些有趣的文章...
Singletons是Pathological Liars有一个单元测试示例,该示例说明了Singletons如何导致很难弄清依赖链以及启动或测试应用程序。这是虐待的一个极端例子,但是他提出的观点仍然有效:
单例仅是全局状态。全局状态使您的对象可以秘密地获取其API中未声明的内容,因此,Singletons使您的API陷入病态的骗子。
Singletons Gone的所有观点都表明,依赖注入使实例易于获得需要它们的构造函数,这减轻了第一篇文章中糟糕的全局Singletons背后的潜在需求。
我认为造成这种混乱的原因是人们不了解Singleton模式的实际应用。我不能太强调这一点。Singleton 不是包装全局变量的模式。单例模式仅应用于确保在运行时存在给定类的一个实例且只有一个实例。
人们认为Singleton是邪恶的,因为他们将其用于全球。正是由于这种困惑,辛格尔顿被轻视了。请不要混淆Singleton和全局变量。如果将其用于预期目的,那么您将从Singleton模式中获得极大的好处。
setInstance
之前的方法。)这并不重要,尽管-这是威尼“被需要”一单也没有知道半导体封装的东西吓到或者有什么错可变的全局状态,因此他很有帮助(?)为每个人提供了设置器。单。领域。(是的,这确实发生了。很多情况。我在野外见过的几乎每个单身人士在设计上都是可变的,而且常常使人尴尬。)
goto
,等他们可能工作在很多情况下,但坦率地说,“作品”是不够的-如果你想违背传统智慧,你最好能够证明如何你的方法为更好地比传统的解决方案。而且我还没有看到Singleton模式的这种情况。
getInstance
或类似名称的方法公开它,并防止a的存在。第二审。坦白说,到那时,您甚至可能没有一个实例。
关于单例的一件相当不好的事情是,您不能很轻松地扩展它们。如果要更改其行为,则基本上必须构建某种装饰器模式或类似的东西。同样,如果有一天您想以多种方式完成某件事,那么根据您对代码的布局方式进行更改可能会非常痛苦。
需要注意的一件事是,如果您确实使用单例,请尝试将其传递给需要它们的人,而不是让他们直接访问它。否则,如果您选择采用多种方式来完成单例,那么改变非常困难,因为如果每个类都直接访问单例,则会嵌入一个依赖项。
所以基本上:
public MyConstructor(Singleton singleton) {
this.singleton = singleton;
}
而不是:
public MyConstructor() {
this.singleton = Singleton.getInstance();
}
我相信这种模式称为依赖注入,通常被认为是一件好事。
就像任何模式一样...考虑它,并考虑在给定情况下使用它是否不合适...通常会违反规则,并且不应该无意地应用这些模式。
getInstance()
,则(b)不再是真的。
getInstance()
,您就可以有效地消除Singleton模式与普通引用之间的一个有用区别。就其余的代码而言,单一性不再是一个属性。只有getInstance()
永远的调用者才需要知道甚至关心多少个实例。仅使用一个调用者,使类可靠地实现单一性要比使调用者简单地存储引用并重用它要付出更多的努力和灵活性。
单例模式本身不是问题。问题在于,这种模式通常由人们使用面向对象的工具来开发软件,而没有扎实地理解OO概念。在这种情况下引入单例时,它们往往会成长为难以管理的类,其中包含每次使用很少的辅助方法。
从测试的角度来看,单例也是一个问题。它们往往使孤立的单元测试难以编写。控制反转(IoC)和依赖项注入是旨在以面向对象的方式克服此问题的模式,可用于单元测试。
在垃圾回收的环境中,单例可能很快成为内存管理方面的问题。
在多线程方案中,单例也可能成为瓶颈以及同步问题。
Singleton
类进行测试。但是,这极大地增加了测试过程。首先,您现在需要能够随意卸载类(在大多数语言中并不是真正的选择),或者为每个测试启动一个新的VM(阅读:测试可能要花费数千倍的时间)。但是,有两个依赖关系Singleton
是一个实现细节,该细节现在正在整个测试中泄漏。
就集群而言,单例也很糟糕。因为这样,您的应用程序中不再有“恰好一个单例”。
请考虑以下情况:作为开发人员,您必须创建一个访问数据库的Web应用程序。为了确保并发数据库调用不会相互冲突,请创建一个线程保存SingletonDao
:
public class SingletonDao {
// songleton's static variable and getInstance() method etc. omitted
public void writeXYZ(...){
synchronized(...){
// some database writing operations...
}
}
}
因此,您可以确保应用程序中仅存在一个单例,并且所有数据库都通过该单例SingletonDao
。现在,您的生产环境如下所示:
到目前为止一切都很好。
现在,考虑要在群集中设置Web应用程序的多个实例。现在,您突然有了以下内容:
听起来很奇怪,但是现在您的应用程序中有很多单例。而这正是单身人士不应该拥有的:拥有很多对象。如本例所示,如果您要对数据库进行同步调用,则尤为糟糕。
当然,这是单例使用不当的一个例子。但是此示例的信息是:您不能依靠您的应用程序中只有一个单例实例-尤其是在集群方面。
垄断是魔鬼,具有非只读/可变状态的单身人士是``真正的''问题...
在阅读了杰森的答案中建议的“ 单身人士是病态骗子”之后,我遇到了这个小窍门,它提供了关于单身人士经常被滥用的最佳例子。
全局是不好的,因为:
- 一个。它导致名称空间冲突
- b。它以不必要的方式暴露了国家
说到单例
- 一个。调用它们的显式OO方式可以防止冲突,因此请指出a。不是问题
- b。没有状态的单身人士(就像工厂一样)不是问题。具有状态的单例可以再次分为两类,一类是不可变的,或者一次写入并读取许多(配置/属性文件)。这些还不错。可变的Singleton,是您所说的参考持有者。
在上一次声明中,他指的是博客的“单身是骗子”的概念。
这如何适用于专卖?
要开始垄断游戏,首先:
现在,对于没有真正扮演垄断者的人,这些标准充其量是最理想的。垄断的失败很难吞噬,因为垄断就是金钱,如果您输了,您就必须努力地看着其余的球员完成比赛,而损失通常是迅速而令人沮丧的。因此,规则通常在某些时候会扭曲,以牺牲某些玩家的利益为某些玩家的自身利益服务。
因此,您正在与朋友Bob,Joe和Ed垄断。您正在迅速建立自己的帝国,并以指数级的速度占领市场份额。您的对手正在减弱,您开始闻到血腥味(象征性地)。您的好友鲍勃(Bob)将所有资金投入了尽可能多的低价值房产,但他没有得到他所期望的高投资回报。鲍勃(Bob)碰运气不佳,落在您的木板路上,并从游戏中删除。
现在,游戏从掷骰子到认真做生意。鲍勃已经成为失败的榜样,乔和埃德不想像“那个家伙”那样结局。因此,作为领先者,您突然变成了敌人。乔和埃德开始练习桌下交易,后台注资,低估房屋交易,以及通常会削弱您作为一名玩家的任何东西,直到其中之一升至最高点。
然后,而不是其中一个获胜,整个过程重新开始。突然,有限的规则集成为了移动目标,游戏逐渐退化为社交互动类型,这构成了自《生还者》以来每部高质量真人秀节目的基础。为什么,因为规则在不断变化,人们对如何代表/为什么/代表什么没有共识,更重要的是,没有人做出决定。那时,游戏中的每个玩家都在制定自己的规则和混乱,直到其中两个玩家过于疲倦以至于无法保持自我并慢慢放弃。
因此,如果游戏规则书准确地代表了一个单例,那么垄断规则书就是滥用的例子。
这如何适用于编程?
除了可变单例所存在的所有明显的线程安全性和同步问题外,如果您拥有一组数据,那么这些数据可以由多个不同的源同时读取/操作,并且在应用程序执行的整个生命周期中都存在,现在可能是退后一步,问“我在这里使用正确类型的数据结构”的好时机。
就个人而言,我已经看到程序员滥用单例,将其用作应用程序中某种形式的扭曲的跨线程数据库存储。直接处理代码后,我可以证明它运行缓慢(由于要使其具有线程安全性,需要进行所有线程锁定)和工作的噩梦(由于同步错误的不可预测/间歇性),以及在“生产”条件下进行测试几乎是不可能的。当然,本来可以使用轮询/信令开发系统来解决某些性能问题,但是这不能解决测试问题,以及为什么当真正的数据库已经可以以更强大的功能完成相同功能时,何必打扰呢? /可扩展方式。
单身是唯一的选择,如果你需要什么单提供。对象的写一次只读实例。相同的规则也应级联到对象的属性/成员。
Singleton.getInstance()
。支持反射的语言可能可以通过设置一个真实实例的存储字段来解决此问题。但是,一旦IMO绕过另一类的私有状态,IMO的测试就变得不那么值得信赖了。
与其他答案不同,我不想谈论Singletons的问题所在,而是向您展示正确使用它们时它们的功能和强大程度!
您可以映射MyModel
到TestMyModel
继承它的类,在任何时候MyModel
注入它们都会使您了解TestMyModel
。- 问题:单例可能会导致内存泄漏,因为它们永远不会被丢弃。
解决方案:好,处置它们!在您的应用中实现回调以正确处理单例,您应该删除链接到它们的所有数据,最后:从工厂中删除它们。
正如我在标题中所述,单例与单个实例无关。
Settings
对象,可从任何小部件访问该对象,以便每个小部件都知道如何格式化显示的数字。如果它不是全局可访问的对象,则必须将其在构造函数中注入到构造函数中的每个小部件,并将对它的引用保留为成员变量。这是对内存和时间的可怕浪费。
我对Singletons如何不好的回答总是,“他们很难做对”。语言的许多基本组件都是单例(类,函数,名称空间甚至是运算符),而其他计算方面的组件(本地主机,默认路由,虚拟文件系统等)也是如此,这并非偶然。尽管它们有时会引起麻烦和沮丧,但它们也可以使很多事情变得更好。
我看到的两个最大的问题是:将其视为全局对象,并且未能定义Singleton闭包。
每个人都将Singleton视为全局变量,因为它们基本上是全局变量。但是,全局中的很多(并不是全部)坏处本质上不是来自于全局,而是您如何使用它。Singletons也是如此。实际上,更多的是“单个实例”,实际上不需要表示“全局可访问”。它是一种自然的副产品,鉴于我们所知道的所有弊端,我们不应该急于利用全球可访问性。一旦程序员看到了Singleton,他们似乎总是总是通过其实例方法直接访问它。相反,您应该像浏览其他任何对象一样导航到该对象。大多数代码甚至都不应该知道它正在处理Singleton(松耦合,对吗?)。如果只有一小段代码像访问全局对象那样访问该对象,则将带来很多危害。
Singleton环境也非常重要。Singleton的定义特征是“只有一个”,但事实是在某种上下文/命名空间中它只有“一个”。它们通常是以下之一:每个线程,进程,IP地址或群集一个,但也可以每个处理器,机器,语言名称空间/类加载器/其他,子网,Internet等一个。
另一个较不常见的错误是忽略Singleton生活方式。仅仅因为只有一个并不意味着一个Singleton是“永远而且永远都会”的万能的,也不是通常所希望的(没有开始和结束的对象违反了代码中各种有用的假设,仅应使用在最绝望的情况下。
如果您避免了这些错误,那么Singletons仍然可以成为PITA,可以随时看到许多最严重的问题都得到了缓解。想象一下一个Java Singleton,它被明确定义为每个类加载器一次(这意味着它需要线程安全策略),具有定义的创建和销毁方法以及一个生命周期,该生命周期规定了何时以及如何调用它们,其“实例”方法具有软件包保护,因此通常可以通过其他非全局对象进行访问。仍然是潜在的麻烦源,但麻烦肯定要少得多。
可悲的是,与其讲授如何做单例的好例子。我们教一些不好的例子,让程序员先用一段时间,然后告诉他们他们是不好的设计模式。
某些人认为它已经被过度使用,因而认为它是反模式,在实际上不需要类的唯一实例的情况下引入了不必要的限制。[1] [2] [3] [4]
参考(仅与文章相关的参考)
并非单例本身很糟糕,而是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<error_logger>().log("Hi there!");
this->another_function(some_other_parameters, srv);
}
克里斯瑞斯(Chris Reath)在“ 不用评论的编码 ”上最近关于这个问题的文章。
注意:“无注释编码”不再有效。但是,链接到的文章已被另一个用户克隆。
单身人士还不错。仅当您使某项全局唯一性变为非全局唯一性时,这才很糟糕。
但是,存在“应用程序范围服务”(想一想使组件进行交互的消息传递系统)-一个单例的此CALLS,即“ MessageQueue”-具有方法“ SendMessage(...)”的类。
然后,您可以从各处进行以下操作:
MessageQueue.Current.SendMessage(new MailArrivedMessage(...));
并且,当然,请执行以下操作:
MessageQueue.Current.RegisterReceiver(this);
在实现IMessageReceiver的类中。
太多的人将不是线程安全的对象放在单例模式中。尽管DataContext不是线程安全的并且纯粹是一个工作单元对象,但我已经看到了以Singleton模式完成的DataContext(LINQ to SQL)示例。
这是关于单身人士的又一件事,但没人说过。
在大多数情况下,“单一性”是某个类的实现细节,而不是其接口的特征。控制容器的反转可能会使类用户看不到此特性。您只需要将您的类标记为单例(@Singleton
例如在Java中带有注释)就可以了。IoCC将完成其余的工作。您不需要提供对单例实例的全局访问,因为该访问已经由IoCC管理。因此,IoC单例没有错。
与IoC Singletons相反的GoF Singletons应该通过getInstance()方法在接口中公开“单一性”,从而使它们遭受上述问题的困扰。
如果您正确且最少地使用它,那么单例并不是邪恶的。还有许多其他好的设计模式可以在某个时候替代单例的需求(并且也可以提供最佳结果)。但是有些程序员没有意识到那些好的模式,并在所有情况下都使用单例,这使单例对他们有害。
首先,班级及其合作者应首先实现其预期目的,而不是专注于家属。生命周期管理(何时创建实例以及何时超出范围)不属于类职责。公认的最佳实践是使用依赖项注入来制作或配置一个新组件来管理依赖项。
通常,软件会变得更加复杂,因此拥有多个具有不同状态的Singleton类的独立实例是有意义的。在这种情况下,提交代码以简单地抓住单例是错误的。Singleton.getInstance()
对于小型简单系统而言,使用可能没问题,但是当可能需要同一类的不同实例时,使用这种方法就行不通。
不应将任何类视为单例,而应该将其用作其用法或如何使用它来配置依赖项。快速而讨厌这没关系-只是硬编码说文件路径并不重要,但对于较大的应用程序,则需要使用DI以更适当的方式分解并管理这种依赖关系。
单例在测试中引起的问题是其硬编码的单用例/环境的症状。测试套件和许多测试都是单独的,它们分离出与单例硬编码不兼容的内容。
因为它们基本上是面向对象的全局变量,所以通常可以以不需要它们的方式来设计类。