单身人士:好的设计还是拐杖?[关闭]


67

单例是一个备受争议的设计模式,因此我对Stack Overflow社区对它们的想法很感兴趣。

请提供您提出意见的理由,而不仅仅是“单字适合懒惰的程序员!”

尽管反对使用Singletons,但关于此问题的文章还是不错的: scienceninja.com:performant-singletons

有人在上面有其他好文章吗?也许支持辛格尔顿?

Answers:


56

捍卫单身人士:

  • 它们没有全局变量那么糟糕,因为全局变量没有标准强制的初始化顺序,并且由于天真或意外的依赖顺序,您很容易看到不确定的错误。单例(假设它们分配在堆上)是在所有全局变量之后创建的,并且在代码中非常容易预测的位置。
  • 它们对于资源延迟/缓存系统(例如到慢速I / O设备的接口)非常有用。如果您智能地为慢速设备构建了单例接口,却没人能调用它,那么您将不会浪费任何时间。如果另一段代码从多个位置调用它,则您的单例可以同时优化两者的缓存,并避免任何重复查找。您还可以轻松避免单例控制资源上的任何死锁情况。

针对单身人士:

  • 在C ++中,没有一种好的方法可以在单例之后自动清除。 有一些变通方法,并且有些怪异的方法,但是没有简单,通用的方法来确保始终调用单例的析构函数。就内存而言,这并不是那么糟糕-为此,可以将其视为更多的全局变量。但是,如果您单身人士分配其他资源(例如,锁定某些文件)并且不释放它们,则可能会很糟糕。

我个人的意见:

我使用单例,但如果有合理的选择,请避免使用它们。到目前为止,这对我来说效果很好,而且我发现它们是可以测试的,尽管要测试的工作要多一些。


5
第一点的假设是一个飞跃,通常堆分配不是如何创建单例的(至少在C ++中)。
Joris Timmermans 2009年

3
我不确定,MadKeithV。有一个标准模式,成员函数(例如MyClass * MyClass :: get_instance())检查静态成员指针,如果它为NULL,则调用new来分配(在堆上)共享实例。单身人士将如何生活在其他地方?
泰勒

关于C ++的缺点:看一下Meyers单例。
保罗·曼塔

2
@Tyler:我刚刚碰到下面的代码,该代码支持@MadKeithV视图,该视图不为单例使用堆。class MySingleton { public: static MySingleton &getInstance() { static MySingleton instance; return instance; } private: MySingleton(); ~MySingleton(); }; 。参考-stackoverflow.com/questions/2593324/c-singleton-class
Forever Learner 2012年

问题标记:“与语言无关”。答案的第一点:“全局变量没有标准强制的初始化顺序,...在所有全局变量之后创建单例(假设它们已分配在堆上)”。这些肯定不是与语言无关的注释。
Mark Amery 2014年

37

Google拥有适用于Java的Singleton Detector,我相信它最初是一种必须在Google生成的所有代码上运行的工具。删除Singleton的简要原因:

因为它们会使测试变得困难并隐藏设计中的问题

有关更明确的说明,请参见Google的“为何单例引起争议”。


我怀疑这是“必须在Google生成的所有代码上运行”的说法。(一方面,他们可能使用其他语言。)
Tyler

我相信Java,Python和C ++是Google认可的语言。
布赖恩

1
他们还可以使用javascript:steve-yegge.blogspot.com/2007/06/rhino-on-rails.html
CTT

2
Google还设有会计部门;这是否意味着每个开发人员都应该获得一份?拒绝某些构造的决定是Google的一项商业决定,它更多地体现了其内部环境。毫无疑问,一些手动调整的机器代码仍然在那里纠结,但出于商业原因,不仅有人应该这样做。
垃圾工

1
去阅读一些谷歌的代码,他们做活此规则。
jkp 2010年

31

单身人士只是化装中的一堆全局变量。

全局变量和单身人士一样都有其用途,但是如果您认为使用单身人士正在做一些很酷且有用的事情,而不是使用讨厌的全局变量(每个人都知道全局变量是不好的mmkay),那么您就会被误导。


1
全局变量...和方法。甚至是一个全球对象……
dqhendricks 2011年

1
单例不一定是真正的全局对象。
iheanyi 2014年

26

Singleton的目的是确保一个类只有一个实例,并提供对其的全局访问点。大多数时候,重点放在单个实例点上。想象一下它是否叫做Globalton。这听起来不那么吸引人,因为它强调了全局变量的(通常)负面含义。

大多数反对单身人士的好论点都与他们在测试中遇到的困难有关,因为为他们创建测试双倍并不容易。


10
那么,为什么一个类要确保自己只有一个实例呢?这几乎破坏了面向对象编程的目的。
Patrick Glandien 09年

3
通常,您只需要一个日志记录工具?或者您正在包装一些voodoo dll,并且不想让两次加载该dll成为可能?
2009年

2
@Calyth,但是当明确地将其作为系统责任时,为什么要将其作为班级的责任呢?(该对象的任何一个实例都可以正常工作,但是如果不满足某个时间唯一的存活条件,则系统可能会失败)
Rune FS

@RuneFS因为我只需要在一个类中编写一次代码即可实现单一性。如果是系统强制执行,则我必须在系统中到处编写代码以强制执行单一性。要测试一个类是否具有一种特定行为,要比测试100个不同类来确保它们都反映出一种行为要容易得多。
iheanyi 2014年

@iheanyi你错过了重点。您已将责任放在“ 100个类”中,这并不是使它成为系统责任的设计。我主张将责任放在负责实例化的系统的任何部分(IoC或类似)
Rune FS 2014年

23

在Google测试博客中,有三篇关于Misko Hevery的Singletons的不错的博客文章。

  1. 单身人士是病态的骗子
  2. 所有的单身人士都去了哪里?
  3. 单身人士的根本原因

1
woow,真的很有趣的东西,但是我想要解决方案!:-) txn
Sander Versluys

4
我认为“解决方案”之一是依赖项注入,它实际上为您处理了实例注入,并减轻了单例成为反模式的麻烦。:)退房吉斯!code.google.com/p/google-guice
Bleadof

1.第一篇文章有​​问题。您可以简单地使单例成为构造函数参数,并完全解决本文中的问题。2. Singleton不必是全局的。因此,用Singleton替换本文中的“共享对象”,一切仍然有效。3.只有当您读到第三篇文章时,他才对冲前两篇文章中所说的所有废话,说:“哦,我要说的坏事是设计模式,在这种模式下,对“单人”的访问才是真正的全球性。 。咄所以,你已经写了3篇文章简单地说,全局变量是坏的,以复杂的方式使用时尤其如此。
iheanyi

20

尽管它被滥用很多,但Singleton并不是一个可怕的模式。我认为这种滥用是因为它是较容易的模式之一,并且对于单例而言,大多数新特性都吸引了它的全局副作用。

埃里希·伽玛(Erich Gamma)曾表示,单身是他希望不包括在GOF书中的一种模式,这是一个糟糕的设计。我倾向于不同意。

如果使用该模式以便在任何给定时间创建一个对象的单个实例,则该模式正在正确使用。如果使用单例以产生全局效果,则使用不正确。

缺点:

  • 您正在调用单例的代码中耦合到一个类
    • 由于很难用模拟对象替换实例,因此在单元测试中造成麻烦
    • 如果由于需要多个实例而在以后需要重构代码,则比将单例类传递给使用该实例的对象(使用接口)要痛苦得多

优点:

  • 类的一个实例在任何给定的时间点表示。
    • 通过设计您正在强制执行此操作
  • 实例在需要时创建
  • 全局访问是一个副作用

4
“全球访问是一个副作用”,我很高兴有人指出这一点。全球方面可能是单例中最被滥用的部分
RobS 2010年

我敢打赌,埃里克·伽玛(Eric Gamma)对单身人士的看法来自许多来之不易的经验。
gtrak 2010年

1
这个答案值得放在IMO的头上。
Frosty Z'9

@RobS,我听不懂。如何在不访问全局实例的情况下使用Singleton,并保持已链接的优势?全局访问不是副作用。
2014年

@Jeffrey最好将这一点称为“全局参考”。原始答案清楚地说明了这一点:“如果以后由于需要多个实例而需要对代码进行重构,则比将单例类传递给对象要痛苦得多”
RobS 2014年

17

小鸡之所以喜欢我,是因为我很少使用单例,而当我这样做时,通常是不寻常的。不,真的,我喜欢单例模式。你知道为什么?因为:

  1. 我很懒。
  2. 没错。

当然,“专家”将围绕“单元测试”和“依赖注入”进行大量讨论,但这充斥着丁戈的肾脏。您说单例很难进行单元测试?没问题!只需公开宣布所有内容,然后将您的课程变成具有全球品味的有趣之屋即可。您还记得1990年代的节目Highlander吗?单身人士之所以这样,是因为:A.它永远不会死;B。只能有一个。因此,请停止收听所有这些DI区域,并放弃您的单例。这里有一些更好的理由...

  • 每个人都在做。
  • 单例模式使您立于不败之地。
  • 具有“获胜”(或“趣味”,取决于您的口音)的单例押韵。

8

我认为使用Singleton模式存在很大的误解。这里的大多数评论都将其称为访问全局数据的地方。我们在这里需要小心-Singleton用于访问全局变量。

Singleton应该仅用于给定类的一个实例模式库Singleton上有很多信息。


6

我与之共事的一位同事非常具有Singleton意识。每当有某种像经理或老板之类的东西时,他都会将其变成单例,因为他认为应该只有一个老板。每次系统提出一些新要求时,事实证明,存在允许多个实例的完全正当理由。

我要说的是,如果领域模型指示(不是“建议”)存在一个,则应该使用单例。所有其他情况只是一个类的单个实例。


5

我一直在试图找到一种方法来拯救可怜的辛格尔顿,但我必须承认这很困难。我见过很少使用它们,并且在当前进行依赖注入和单元测试的驱动器中,它们很难使用。它们绝对是我与许多从未破解过“ GoF”书的程序员一起工作的具有设计模式的编程的“货物崇拜”体现,但他们知道“ Singelton”,因此也知道了“ Patterns”。

不过,我确实不同意Orion,在大多数情况下,我都看到singeltons过度怀疑这不是衣服上的全局变量,而更像衣服上的全局服务(方法)。有趣的是,如果您尝试通过CLR接口以安全模式在SQL Server 2005中使用Singeltons,则系统将标记该代码。问题是您拥有的持久数据超出了可能运行的任何给定事务,当然,如果您将实例变量设置为只读,则可以解决此问题。

这个问题导致我一年的工作量很大。


2
我从来不明白为什么人们如此“喜欢”这种模式。我猜这是因为它“允许”您使用全局变量,从而省去了重新考虑设计的麻烦。当然,除了不是。
克里斯·黄·利弗

5

圣战!好,让我看看。上次我检查设计警察时说。

单例很糟糕,因为它们阻碍了自动测试-无法为每个测试用例重新创建实例。相反,逻辑应该在可以轻松实例化和测试的(A)类中。另一个类(B)应该负责约束创建。单一责任原则脱颖而出!应该知道,您应该通过B来访问A,这是一种团队惯例。

我大部分都同意。


4

许多应用程序要求某个类只有一个实例,因此仅具有一个类的实例的模式很有用。但是该模式的实现方式有所不同。

有一个静态单例,其中类强制每个进程只能有一个该类的实例(在Java中实际上是每个ClassLoader一个)。另一种选择是仅创建一个实例

静态单例是邪恶的-一种全局变量。它们使测试更加困难,因为不可能完全隔离地执行测试。您需要复杂的设置和拆卸代码才能在每次测试之间清洗系统,并且很容易忘记正确清洗某些全局状态,这又可能导致测试中出现未指定的行为。

只创建一个实例是好的。您只需在程序启动时创建一个实例,然后将指向该实例的指针传递给需要它的所有其他对象。依赖注入框架使此操作变得容易-只需配置对象的范围,DI框架将负责创建实例并将其传递给所有需要它的人。例如,在Guice中,您将使用@Singleton注释该类,并且DI框架将仅创建该类的一个实例(每个应用程序-您可以在同一JVM中运行多个应用程序)。这使测试变得容易,因为您可以为每个测试创建该类的新实例,并让垃圾回收器在不再使用该实例时销毁该实例。全局状态不会从一项测试泄漏到另一项测试。

有关更多信息: 清洁代码讨论-“全局状态和单例”


关于在两次测试之间清洁此类物体的原因,为什么要使其复杂化?为什么不能销毁该物体,而在其中旋转一个新物体呢?对此没有任何便利是实现问题,而不是模式本身的问题。
垃圾工

问题在于要记住在测试后始终清洁对象。另外,由于代码可以从任何地方访问静态单例,因此仅通过查看测试代码就无法查看某个类是否使用静态单例,因此很容易忘记某些依赖项。将此与依赖注入进行比较,在依赖注入中,必须在构造函数中显式声明所有依赖-如果忘记了某些依赖,则代码甚至都不会编译。
Esko Luontola'2

3

Singleton作为实现细节很好。Singleton作为接口或访问机制是一个巨大的PITA。

不带参数返回对象实例的静态方法与仅使用全局变量仅稍有不同。相反,如果某个对象通过构造函数或其他方法引用了传入的单例对象,则该单例的实际创建方式与整个模式无关紧要无关紧要。


2

这不仅是一堆花哨的变量,因为它要承担许多责任,例如与持久层进行通信以保存/检索有关公司的数据,处理员工和收取价格等。

我必须说,您并不是真正在描述应该是单个对象的东西,并且有争议的是,除了数据序列化之外,其他任何东西都应该是一个罪魁祸首。

我可以看到我通常会设计的至少3组类,但是我倾向于使用较小的简单对象,它们很好地完成了一组狭窄的任务。我知道这不是大多数程序员的本性。(是的,我每天处理5000种线路类的怪兽,并且我特别喜欢某些人编写的1200条线路方法。)

我认为关键是,在大多数情况下,您不需要singelton,而通常只是您的生活变得更加艰难。


2

单例的最大问题是,它们使单元测试变得困难,尤其是当您要并行但独立地运行测试时。

第二个原因是,人们通常认为带有双重检查锁定的惰性初始化是实现它们的好方法。

最后,除非单例是不可变的,否则当您尝试扩展应用程序以在多个处理器上的多个线程中运行时,它们很容易成为性能问题。竞争性同步在大多数环境中都很昂贵。


有人愿意分享拒绝投票的原因吗?
比尔·米歇尔

比尔,我不得不说我也想知道吗?我经验不足,但是您对并发问题的观点对我来说合乎逻辑吗?
SteinÅsmul'2009年

在懒惰的初始化中使用单例有什么问题,例如单例是一个助手?我并不是说你错了,我只是在问。
Flavius 2010年

问题在于使用双重检查锁定来初始化它们。请参阅en.wikipedia.org/wiki/Double-checked_locking-例如,在Java中,简单的双重检查实现不是线程安全的,因此可能导致难以诊断的故障。
比尔·米歇尔

1

单例有其用途,但是在使用和公开它们时必须小心,因为它们太容易滥用,很难真正地进行单元测试,并且很容易基于两个互相访问的单例创建循环依赖关系。

但是,这对您很有帮助,因为当您要确保所有数据在多个实例之间同步时,例如,分布式应用程序的配置可能依赖单例来确保所有连接使用相同的最新数据。日期数据集。


单身人士正在混合责任。为了确保某事物只有X个实例是系统责任,而无论对象的功能是什么,这都是一个对象责任。
符文FS 2014年

1

我发现您在决定为什么使用单例时必须非常小心。正如其他人提到的那样,这与使用全局变量本质上是相同的问题。您必须非常谨慎,并考虑使用一个可以做什么。

使用它们非常罕见,通常有更好的方法来做事。我遇到过这样的情况:我单身完成某件事,然后在发现造成问题的糟糕程度之后(或者在我提出更好,更理智的解决方案之后)不得不仔细检查我的代码以将其取出)


1

我与Spring一起使用过多次Singleton ,但并不认为它是拐杖或懒惰的。

这种模式允许我做的是为一堆配置类型的值创建一个单一的类,然后在我的Web应用程序的多个用户之间共享该特定配置实例的单个(非可变)实例。

就我而言,单例包含特定于该客户端的客户端配置条件-css文件位置,数据库连接条件,功能集等。这些类是通过Spring实例化和访问的,并由具有相同配置的用户(即,来自同一公司的2个用户)共享。* **我知道这类应用程序有个名字,但它在逃避我*

我觉得为应用程序的每个用户创建(然后垃圾收集)这些“常量”对象的新实例会很浪费。


这是一种好的方法。问题是单例的静态访问器。
gtrak 2010年

1

我正在阅读有关“ Singleton”,它的问题,何时使用等的很多知识,到目前为止,这些是我的结论:

  • Singleton的经典实现与实际要求之间的混淆:只需要一个类的实例!

  • 通常很难实现。如果需要唯一的实例,请不要使用使用静态GetInstance()方法返回静态对象的(反)模式。这使一个类负责实例化自身的单个实例并执行逻辑。这打破了单一责任原则。相反,这应该由工厂类实现,并负责确保仅存在一个实例。

  • 它在构造函数中使用,因为它易于使用,并且不能作为参数传递。这应该使用依赖注入来解决,这是实现良好且可测试的对象模型的好模式。

  • 不是TDD。如果您执行TDD,则会从实现中提取依赖项,因为您希望测试易于编写。这使您的对象模型变得更好。如果使用TDD,则不会编写静态的GetInstance =)。顺便说一句,如果您认为职责明确的对象而不是类,您将获得相同的效果=)。


请拒绝投票的人说清楚其拒绝投票的理由吗?感谢您的反馈!
bloparod

0

我真的不同意在化装服装创意中的一堆全局变量。当用于解决正确的问题时,单例确实非常有用。让我给你一个真实的例子。

我曾经在我工作的地方开发过一小段软件,有些表格必须使用有关公司,员工,服务和价格的信息。在第一个版本中,每次打开表单时,系统都会从数据库中加载该数据。当然,我很快意识到这种方法不是最好的方法。

然后,我创建了一个单例课程,命名为company封装了该地点的所有内容,并且在系统打开时已完全填充了数据。

这不仅是一堆花哨的变量,因为它要承担许多责任,例如与持久层进行通信以保存/检索有关公司的数据,处理员工和收取价格等。

另外,它是拥有公司数据的固定的,系统范围的,易于访问的点。


2
我没有对此进行否决,但是我想解释为什么有人可能对此进行了否决。Singlton不是缓存数据的最佳/唯一方法。负责数十种职责的班级不是“正确”设计的。
艾米B

听起来您的单例应该完全是一个单独的程序。接口是其服务层。
gtrak

0

单例非常有用,使用它们本身并不是反模式。但是,它们之所以声名狼藉,是因为它们迫使任何使用代码的人都承认它们是单例的,以便与它们进行交互。这意味着,如果您需要“取消单一化”它们,则对您的代码库的影响可能非常大。

相反,我建议您将Singleton藏在工厂后面。这样,如果将来需要更改服务的实例化行为,则只需更改工厂,而不是更改所有使用Singleton的类型。

更好的是,使用控制容器的反转!它们中的大多数允许您将实例化行为与类的实现分开。


0

例如在Java中,单例的一件令人恐惧的事情是,在某些情况下,您可能会遇到同一单例的多个实例。JVM基于两个元素唯一标识:类的完全限定名称,以及负责加载它的类加载器。

这意味着可以由两个彼此不知道的类加载器加载同一个类,并且应用程序的不同部分将具有与之交互的此单例的不同实例。


如果两个类加载器加载了相同的类,则它不是同一类。
汤姆·霍顿

如果该类由类加载器A和B加载,但它们都实现了由类加载器C加载的相同接口,那么您可以将类的实例从类加载器A和B传递给C的用户。因此,程序可以使用a的两个实例。同时单身。
Esko Luontola 09年

0

编写正常的,可测试的,可注入的对象,并让Guice / Spring /任何对象处理实例化。说真的

这甚至适用于高速缓存或单例的自然使用情况。无需重复编写代码即可尝试强制执行一个实例的恐怖。让您的依赖项注入框架来处理它。(如果您尚未使用Guice,建议您为轻量级的DI容器使用Guice)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.