单身人士有什么不好呢?
单例模式是GoF的模式书的全额付费会员,但最近似乎被开发人员世界孤立了。我仍然使用很多单例,尤其是对于工厂类,并且尽管我们必须对多线程问题(实际上是任何类)有所注意,但我看不出它们为什么如此糟糕。
堆栈溢出似乎特别假设每个人都同意Singletons是邪恶的。为什么?
请以"事实,参考或者特定专业知识"来支持答案
解决方案
因为它们基本上是面向对象的全局变量,所以通常可以以不需要它们的方式设计类。
- 它很容易用作全局变量。
- 依赖单例的类相对难以单独进行单元测试。
参见维基百科Singleton_pattern
It is also considered an anti-pattern by some people, who feel that it is overly used, introducing unnecessary limitations in situations where a sole instance of a class is not actually required.[1][2][3][4]
参考(仅与文章相关的参考)
- ^亚历克斯·米勒。我讨厌的模式1:Singleton,2007年7月
- ^ Scott Densmore。为什么单身人士是邪恶的,2004年5月
- ^史蒂夫·耶格(Steve Yegge)。 2004年9月,单身汉被认为是愚蠢的
- ^ J.B. Rainsberger,IBM。明智地使用单身人士,2001年7月
假设模式正用于单个模型的某个方面,那么模式就没有本质上的问题。
我认为,这种抵制是由于其过度使用,而这又是由于它是最易于理解和实施的模式。
来自Google的Misko Hevery就此主题发表了一些有趣的文章...
Singletons是Pathological Liars有一个单元测试示例,该示例说明了Singletons如何使得很难弄清依赖链以及启动或者测试应用程序。这是虐待的一个极端例子,但他提出的观点仍然有效:
Singletons are nothing more than global state. Global state makes it so your objects can secretly get hold of things which are not declared in their APIs, and, as a result, Singletons make your APIs into pathological liars.
Singletons Gone的所有观点都表明,依赖注入使实例易于获得需要它们的构造函数,从而减轻了第一篇文章中所否定的,糟糕的全局Singletons背后的潜在需求。
它模糊了关注点的分离。
假设我们有一个单例,则可以从类内部的任何位置调用此实例。课程不再像原来那样纯净。现在,班级将不再对其成员以及它明确接收的成员进行操作。这将造成混乱,因为班级的用户不知道班级需要的足够信息是什么。封装的整个想法是向用户隐藏一种方法的方式,但是如果在方法内部使用单例,则必须知道单例的状态才能正确使用该方法。这是反面向对象的。
使用静态方法来实现单例。进行单元测试的人员应避免使用静态方法,因为它们不能被模拟或者存根。该站点上的大多数人都是单元测试的大力支持者。避免它们的最普遍接受的约定是使用控制模式的反转。
一些编码小工具视它们为荣耀的全球对象。就像许多人讨厌goto语句一样,还有其他人讨厌曾经使用global的想法。我已经看到几个开发人员竭尽全力避免全局,因为他们考虑将开发人员视为失败的承认。奇怪但真实。
实际上,Singleton模式只是一种编程技术,它是概念工具包的有用组成部分。我们可能会不时发现它是理想的解决方案,因此请使用它。但是使用它只是为了夸耀使用设计模式就如同拒绝使用它一样愚蠢,因为它只是全局的。
布赖恩·巴顿(Brian Button)的释义:
- 它们通常用作全局实例,为什么这么糟?因为我们在代码中隐藏了应用程序的依赖关系,而不是通过接口公开它们。使某些东西全局化以避免传递它是代码的味道。
- 他们违反了单一责任原则:由于他们控制自己的创作和生命周期。
- 它们固有地导致代码紧密耦合。在许多情况下,这使得将它们伪装成测试对象相当困难。
- 它们在应用程序的整个生命周期中都带有状态。测试的另一个问题是,我们可能会遇到需要订购测试的情况,这对单元测试来说是一个很大的"不行"。为什么?因为每个单元测试应该彼此独立。
当几个人(或者团队)得出相似或者相同的解决方案时,就会出现一种模式。许多人仍然使用原始形式的单例或者使用工厂模板(Alexandrescu的Modern C ++ Design中有很好的讨论)。并发性和管理对象生存期的困难是主要障碍,前者很容易按照建议进行管理。
像所有选择一样,Singleton也有其起伏不定的份额。我认为可以适度使用它们,尤其是对于那些在应用程序生命周期中仍然存在的对象。它们类似于(很可能是)全局变量的事实可能引起了纯粹主义者的注意。
关于单例的一件相当不好的事情是,我们不能很轻松地扩展它们。如果我们想更改其行为,则基本上必须构建某种装饰器模式或者类似的东西。同样,如果有一天我们想以多种方式完成一项任务,那么根据我们对代码的布局方式进行更改可能会非常痛苦。
需要注意的一件事是,如果我们确实使用单例,请尝试将它们传递给需要它们的人,而不是让他们直接访问它。否则,如果我们选择采用多种方式来完成单例所做的事情,那将是改变非常困难,因为如果每个类都直接访问单例,则会嵌入一个依赖项。
所以基本上:
public MyConstructor(Singleton singleton) { this.singleton = singleton; }
而不是:
public MyConstructor() { this.singleton = Singleton.getInstance(); }
我相信这种模式称为依赖注入,通常被认为是一件好事。
就像任何模式一样……考虑它,并考虑在给定情况下使用它是否不合适……通常会违反规则,并且不应无意间应用这些模式。
当我们使用单例(例如记录器或者数据库连接)编写代码时,随后发现我们需要多个日志或者多个数据库,这很麻烦。
单例使从它们移动到常规对象变得非常困难。
同样,编写非线程安全的单例也太容易了。
而不是使用单例,我们应该在函数之间传递所有必需的实用程序对象。如果将它们全部包装到一个辅助对象中,则可以简化此过程,如下所示:
void some_class::some_function(parameters, service_provider& srv) { srv.get<error_logger>().log("Hi there!"); this->another_function(some_other_parameters, srv); }
Singleton的反模式!马克·拉德福德(Mark Radford)(Overload Journal#57 Oct 2003)很好地解释了为何将Singleton视为反模式。本文还讨论了替代Singleton的两种替代设计方法。
从我的头顶上:
- 他们执行紧密耦合。如果单身人士与其用户所在的装配体不同,则使用的装配体如果没有包含单身人士的装配体将永远无法发挥作用。
- 它们允许循环依赖关系,例如,程序集A可以具有一个依赖于程序集B的单例,而程序集B可以使用程序集A的单例。所有这些都不会破坏编译器。
单例模式本身不是问题。问题在于,这种模式通常由人们使用面向对象的工具来开发软件,而没有扎实地理解OO概念。在这种情况下引入单例时,它们往往会变成难以管理的类,其中包含每次使用很少的辅助方法。
从测试的角度来看,单例也是一个问题。它们往往使孤立的单元测试难以编写。控制反转(IoC)和依赖项注入是旨在以面向对象的方式克服此问题的模式,可用于单元测试。
在垃圾回收的环境中,单例可能很快成为内存管理方面的问题。
在多线程方案中,单例也可能成为瓶颈以及同步问题。
我不会评论善恶论据,但是自从Spring出现以来,我就没有使用过它们。使用依赖注入几乎消除了我对单例,服务定位器和工厂的需求。我发现这至少在我从事的工作类型(基于Java的Web应用程序)上是一个更加高效和干净的环境。
太多的人将不是线程安全的对象放在单例模式中。我已经看过以单例模式完成DataContext(LINQtoSQL)的示例,尽管DataContext不是线程安全的,并且纯粹是一个工作单元对象。
Singleton是一种模式,可以像其他任何工具一样使用或者滥用。
单例的坏部分通常是用户(或者我应该说,单例不适合其设计用途的不适当使用)。最大的罪犯是使用单例作为伪造的全局变量。
我认为造成这种混乱的原因是人们不了解Singleton模式的实际应用。我不能太强调这一点。 Singleton不是包装全局变量的模式。单例模式仅应用于确保在运行时存在给定类的一个实例,并且只有一个实例。
人们认为Singleton是邪恶的,因为他们将其用于全球。正是由于这种困惑,辛格尔顿被轻视了。请不要混淆Singleton和全局变量。如果将其用于预期目的,则可以从Singleton模式中获得极大的好处。
单例解决一个(也是唯一一个)问题。
资源争用。
如果我们有一些资源
(1)只能有一个实例,并且
(2)我们需要管理单个实例,
我们需要一个单身人士。
例子不多。日志文件很大。我们不想只放弃一个日志文件。我们要正确刷新,同步并关闭它。这是必须管理的单个共享资源的示例。
我们很少需要单身人士。他们之所以不好,是因为他们感觉自己像个全球人,并且是GoF Design Patterns书中的全薪会员。
当我们认为自己需要一个整体时,我们可能会犯下一个可怕的设计错误。
克里斯·瑞思(Chris Reath)在Coding Without Comments上有关该主题的最新文章。
注意:"无注释编码"不再有效。但是,链接到的文章已被另一个用户克隆。
http://geekswithblogs.net/AngelEyes/archive/2013/09/08/singleton-i-love-you-but-youre-bringing-me-down-re-up-loaded.aspx
作者的一些反驳:
如果将来需要使班级不再单身,我们将陷入困境
在这种情况下,我根本不想将单个数据库连接单例变成一个连接池。
请记住,每个单例都是通过标准方法访问的:
MyClass.instance
这类似于工厂方法的签名。我所做的就是更新实例方法以从池中返回下一个连接,而无需进行其他更改。如果我们不使用单例,那将更加困难。
单身人士只是幻想中的全球人
不能对此表示怀疑,但是从类而不是从实例访问的所有静态字段和方法是否也本质上是全局的,并且我看不到使用静态字段的太多推销?
并不是说辛格尔顿一家人很好,只是在这里推了一些"传统智慧"。
单身人士的问题是范围扩大并因此耦合的问题。不可否认,在某些情况下我们确实需要访问单个实例,并且可以通过其他方式来实现。
我现在更喜欢围绕控制反转(IoC)容器进行设计,并允许生命周期由容器控制。这为我们提供了依赖实例的类的好处,而不必知道只有一个实例。单例的生存期可以在将来更改。我最近遇到的这样的例子是从单线程到多线程的简单调整。
FWIW,如果在尝试对其进行单元测试时是PIA,则在尝试对其进行调试,错误修复或者增强时将转到PIA。
首先,班级及其合作者应首先实现其预期目的,而不是专注于后代。生命周期管理(当实例超出范围时会被警告)时,不应将其视为clads职责的一部分。公认的最佳实践是使用依赖项注入来制作或者配置一个新组件来管理依赖项。
通常,软件变得更加复杂,有意义的是具有状态不同的" singleton"类的多个独立实例。在这种情况下,提交代码以简单地抓住单例是错误的。对于小型简单系统,可以使用Singleton.getInstance(),但是当可能需要同一类的不同实例时,它不起作用/无法扩展。
不应将任何类视为单例,而应将其用作其用法或者如何使用它来配置依赖项。快速而讨厌这并不重要,只需硬编码就说文件路径无关紧要,但对于较大的应用程序,则需要使用DI以更适当的方式来分解和管理这种依赖关系。
单例在测试中引起的问题是其硬编码的单用例/环境的症状。测试套件和许多测试都是单独的,它们分离出与单例硬编码不兼容的内容。
到目前为止,我认为这是缺少的答案:
如果每个进程地址空间都需要该对象的一个实例(并且我们确信该要求不会改变),则应将其设为单例。
否则,它不是单例。
这是一个非常奇怪的要求,用户几乎没有兴趣。进程和地址空间隔离是实现细节。它们仅在希望使用kill或者Task Manager停止应用程序时对用户产生影响。
除了构建缓存系统外,没有太多原因使我们可以确定每个进程只能包含一个实例。日志系统怎么样?最好是每个线程或者更细粒度,以便我们可以更自动地跟踪消息的来源。该应用程序的主窗口如何?这取决于;也许出于某种原因,我们希望所有用户文档都由同一过程管理,在这种情况下,该过程中将有多个"主窗口"。
文斯·休斯顿(Vince Huston)具有以下标准,在我看来,这是合理的:
Singleton should be considered only if all three of the following criteria are satisfied: Ownership of the single instance cannot be reasonably assigned Lazy initialization is desirable Global access is not otherwise provided for If ownership of the single instance, when and how initialization occurs, and global access are not issues, Singleton is not sufficiently interesting.
我想谈谈接受的答案中的4点,希望有人可以解释我为什么做错了。
- 为什么在代码中隐藏依赖关系不好?已经有许多隐藏的依赖项(C运行时调用,OS API调用,全局函数调用),单例依赖项很容易找到(搜索instance())。 "使全局事物避免传递出去是一种代码气味。"为什么不传递某些内容以避免将其变成单例代码的味道?如果要通过一个调用堆栈中的10个函数传递一个对象只是为了避免单例,那太好了吗?
- 单一责任原则:我认为这有点含糊,取决于我们对责任的定义。一个相关的问题是,为什么在班级中增加这种特定的"责任"很重要?
- 为什么将对象传递给类比将对象作为类中的单例对象更紧密地耦合呢?
- 为什么它会改变状态持续多长时间?可以手动创建或者销毁单例,因此控件仍然存在,并且可以使生存期与非单一对象的生存期相同。
关于单元测试:
- 并非所有类都需要进行单元测试
- 并非所有需要进行单元测试的类都需要更改单例的实现
- 如果确实需要对它们进行单元测试并且确实需要更改实现,则很容易将类从使用单例更改为通过依赖注入将单例传递给它。
这是关于单身人士的又一件事,但没人说过。
在大多数情况下,"单一性"是某个类的实现细节,而不是其接口的特征。控制容器的倒置可能会使类用户看不到这个特性。我们只需要将类标记为单例(例如,在Java中带有@@ Singleton`注释)就可以了;仅此而已; IoCC将完成其余的工作。我们不需要提供对单例实例的全局访问,因为该访问已经由IoCC管理。因此,IoC Singletons没有任何问题。
与IoC Singletons相反的GoF Singletons应该通过getInstance()方法在接口中公开"单一性",因此它们遭受了上述所有问题。
单身人士还不错。仅当我们使某项全局唯一性变得不全局唯一性时,这才是不好的。
但是,对于单个实例,此CALLS有"应用程序范围服务"(想使组件交互的消息传递系统),即具有方法" SendMessage(...)"的" MessageQueue"类。
然后,我们可以从各处进行以下操作:
MessageQueue.Current.SendMessage(new MailArrivedMessage(...));
并且,当然,请执行以下操作:
MessageQueue.Current.RegisterReceiver(this);
在实现IMessageReceiver的类中。
垄断是魔鬼,具有非只读/可变状态的单身人士是``真正的''问题...
在阅读了杰森的答案中建议的"单身人士是病理骗子"之后,我碰到了这个小窍门,它提供了关于单身人士经常被滥用的最佳例子。
Global is bad because: a. It causes namespace conflict b. It exposes the state in a unwarranted fashion When it comes to Singletons a. The explicit OO way of calling them, prevents the conflicts, so point a. is not an issue b. Singletons without state are (like factories) are not a problem. Singletons with state can again fall in two categories, those which are immutable or write once and read many (config/property files). These are not bad. Mutable Singletons, which are kind of reference holders are the ones which you are speaking of.
在上一次声明中,他指的是博客的"单身是骗子"的概念。
这如何适用于专卖?
要开始垄断游戏,首先:
- 我们首先建立规则,以便每个人都在同一页面上
- 在游戏开始时,每个人都有一个平等的起点
- 仅提出一组规则以避免混淆
- 在整个游戏中不允许更改规则
现在,对于那些还没有真正扮演垄断者的人,这些标准充其量是最理想的。垄断的失败很难被吞噬,因为垄断就是金钱,如果我们输了,我们就必须认真地看着其余的球员完成比赛,而损失通常是迅速而令人沮丧的。因此,规则通常会在某些时候扭曲,以牺牲某些玩家的利益为某些玩家的自身利益服务。
因此,我们正在与朋友Bob,Joe和Ed一起垄断游戏。我们正在迅速建立自己的帝国,并以指数级的速度占领市场份额。对手正在减弱,我们开始闻到血腥味(象征性地)。好友鲍勃(Bob)将所有钱都投入了尽可能多的低价房产,但他没有得到他所期望的高投资回报率。鲍勃(Bob)倒霉,落在木板路上并将其从游戏中删除。
现在,游戏从掷骰子到认真做生意。鲍勃已经成为失败的榜样,乔和埃德不想像"那个家伙"那样结局。因此,作为领先者,我们突然变成了敌人。乔和埃德开始练习桌下交易,后台注资,低估房屋交易,以及通常会削弱我们作为一名玩家的任何东西,直到其中之一升至最高点。
然后,而不是其中一个赢得胜利,整个过程重新开始。突然,一组有限的规则成为一个移动目标,游戏逐渐退化为社交互动类型,这构成了自《生还者》以来每部高质量真人秀节目的基础。为什么,因为规则在不断变化,并且对于如何/为什么/代表什么没有达成共识,更重要的是,没有人做出决定。那时,游戏中的每个玩家都在制定自己的规则和混乱,直到其中两个玩家过于疲倦以至于无法保持自我并慢慢放弃。
因此,如果游戏规则书准确地代表了一个单例,那么垄断规则书就是滥用的一个例子。
这如何适用于编程?
除了可变单例所带来的所有明显的线程安全和同步问题外,如果我们拥有一组数据,那么这些数据可以由多个不同的源同时读取/操作,并且在应用程序执行的整个生命周期中都存在,现在可能是退后一步,问"我在这里使用正确类型的数据结构"的好时机。
就个人而言,我已经看到程序员滥用单例,将其用作应用程序中某种形式的扭曲的跨线程数据库存储。直接处理代码后,我可以证明它运行缓慢(由于需要所有线程锁定才能使其具有线程安全性)和工作的噩梦(由于同步错误的不可预测/间歇性),以及在"生产"条件下进行测试几乎是不可能的。当然,本来可以使用轮询/信令开发系统来解决某些性能问题,但这不能解决测试问题,以及为什么当真正的数据库已经可以以更强大的功能完成相同功能时,何必打扰呢? /可扩展方式。
仅当我们需要单例提供的内容时,单例才是一个选择。对象的写一次只读实例。相同的规则也应级联到对象的属性/成员。