关于设计模式:什么时候应该使用单例?


447

荣耀的全局变量-成为荣耀的全局类。有人说打破了面向对象的设计。

给我一些方案,除了一个好的老记录器之外,使用单例是有意义的。


4
由于学习了erlang,我更喜欢这种方法,即不变性和消息传递。
Setori

209
这个问题没有什么建设性的?我在下面看到有建设性的回答。
mk12 2013年

3
依赖项注入框架是一个非常复杂的单例,它发出对象…。
伊恩·林罗斯

1
可以将Singleton用作其他对象实例之间的管理器对象,因此,应该只有Singleton的一个实例,其他实例之间应通过Singleton实例进行通信。
Levent Divilioglu 2015年

我有一个附带的问题:任何Singleton实现也可以使用“静态”类(通过“ factory” /“ init”方法)实现-无需实际创建类的实例(您可以说静态类是类的Singleton实现,但是...)-为什么应该使用实际的Singleton(确保其唯一的单个类实例)而不是静态类?我能想到的唯一原因可能就是“语义”,但即使从这个意义上讲,Singleton用例实际上并不需要定义上的“类->实例”关系...所以...为什么呢?
Yuval A.

Answers:


358

在寻求真相的过程中,我发现实际上很少有使用“单例”的“可接受”理由。

互联网上反复出现的一个原因是“记录”类的原因(您提到过)。在这种情况下,可以使用Singleton代替类的单个实例,因为项目中的每个类通常需要一遍又一遍地重复使用日志记录类。如果每个类都使用此日志记录类,则依赖项注入将变得很麻烦。

日志记录是“可接受的”单例的特定示例,因为它不会影响代码的执行。禁用日志记录,代码执行保持不变。启用它,相同。Misko在“ 单身人士的根本原因 ” 中以以下方式表示:“信息以一种方式流动:从您的应用程序进入记录器。即使记录器处于全局状态,由于没有信息从记录器流入您的应用程序,因此记录器是可以接受的。”

我敢肯定还有其他正当理由。Alex Miller在“ 我讨厌的模式 ”一文中谈到了服务定位器和客户端UI也可能是“可接受的”选择。

在Singleton中阅读更多内容我爱你,但你让我失望。


3
@ArneMertz我想是一个。
2013年

1
为什么不能只使用全局对象?为什么必须是单身?
Shoe 2015年

1
我认为用于日志记录实用程序的静态方法?
天网

1
当您需要管理资源时,单例是最好的。例如,Http连接。您不想为单个客户端建立100万个http客户端,这是疯狂的浪费和缓慢。因此,具有连接池的http客户端的单例将更快且对资源友好。
Cogman

3
我知道这是一个古老的问题,并且此答案中的信息非常有用。但是,当OP明确指定时,我很难理解为什么这是一个可以接受的答案:“给我方案,而不是使用单例的老旧记录器。”
Francisco C.

124

单身人士候选人必须满足三个要求:

  • 控制对共享资源的并发访问。
  • 将要求系统的多个不同部分访问资源。
  • 只能有一个物体。

如果您提出的Singleton仅具有这些要求中的一个或两个,那么重新设计几乎总是正确的选择。

例如,一个打印机后台处理程序不太可能从多个位置调用(“打印”菜单),因此您可以使用互斥锁解决并发访问问题。

一个简单的记录器是一个可能有效的Singleton的最明显的例子,但是随着更复杂的记录方案的改变,这种情况可能会改变。


3
我不同意第2点。第3点并不是一个真正的原因(只是因为您可以做到这并不意味着您应该这样做),第1点是一个好点,但是我仍然看不到它的用处。可以说共享资源是磁盘驱动器或数据库缓存。您可以添加另一个驱动器,也可以具有针对另一件事的数据库高速缓存(例如,一个线程专用表的高速缓存,而另一个线程则更通用)。

15
我认为您错过了“候选人”一词。单身人士候选人必须满足三个条件;仅仅因为某些东西符合要求,并不意味着它应该是一个Singleton。可能还有其他设计因素:)
metao 2011年

45

读取仅应在启动时读取的配置文件,并将其封装在Singleton中。


8
类似于Properties.Settings.Default.NET。
Nick Bedford

9
@Paul,“ no-singleton阵营”将指出,应仅将配置对象传递给需要它的函数,而不是使其可以全局访问(也称为单例)。
起搏器

2
不同意。如果将配置移到数据库,一切都搞砸了。如果配置路径依赖于那个单例之外的任何东西,那么这些东西也必须是静态的。
rr-

3
@PaulCroarkin您可以对此进行扩展并解释这有什么好处吗?
AlexG '17

1
@ rr-如果配置移至数据库,则仍可以将其封装在配置对象中,该对象将传递给需要它的函数。(PS我不在“无单人”阵营中)。
Will Sheppard

36

当您需要管理共享资源时,可以使用单例。例如打印机后台处理程序。您的应用程序应仅具有一个后台处理程序实例,以避免对相同资源的请求冲突。

或数据库连接或文件管理器等。


30
我听过这个打印机后台处理程序示例,我认为这有点la脚。谁说我不能有多个后台处理程序?无论如何,后台打印程序到底是什么?如果我拥有不冲突或使用不同驱动程序的不同类型的打印机该怎么办?
1800信息

6
它只是一个示例...对于任何人用作示例的任何情况,您都可以找到使该示例无用的替代设计。假设假脱机程序管理由多个组件共享的单个资源。有用。
Vincent Ramdhanie,

2
这是“四人帮”的经典例子。我认为用实际试用过的用例回答会更有用。我的意思是您实际上认为Singleton是最好的解决方案。
Andrei Vajna II

在我看来,共享资源是一个过于宽泛的例子。当您不能注入有故障的“后台处理程序”实现时,如何面对后台处理程序故障使用打印后台处理程序的对象正常工作?尽管简短而无用,但在我的书中,可接受的答案是非常安全的方法
Rune FS 2010年

打印机后台处理程序到底是什么?
RayLoveless

23

只读的单例存储一些全局状态(用户语言,帮助文件路径,应用程序路径)是合理的。小心使用单例来控制业务逻辑-单个几乎总是以多个结尾


4
用户语言只能是单例,并假设只有一个用户可以使用系统。
塞缪尔·奥斯隆2014年

…而一位用户只会说一种语言。
光谱

17

管理与数据库的连接(或连接池)。

我还将使用它来检索和存储有关外部配置文件的信息。


2
数据库连接生成器是否不是Factory的示例?
肯(Ken)

3
@Ken,在几乎所有情况下,您都希望该工厂成为单例。
克里斯·马里西奇

2
@Federico,“非单例阵营”将声明应将这些数据库连接简单地传递给需要它们的函数,而不是使其在全局范围内可访问(也称为单例)。
起搏器

3
您实际上不需要单例。可以注射。
Nestor Ledon

11

使用单例的一种方法是覆盖必须有一个“代理”来控制对资源访问的实例。单例在记录器中很有用,因为它们代理访问某个文件的访问,该文件只能以独占方式写入。对于诸如日志之类的东西,它们提供了一种将写入内容抽象为诸如日志文件之类的方式-您可以将缓存机制包装到单例中,等等。

还要考虑这样一种情况,您的应用程序具有许多Windows /线程/等,但是需要单点通信。我曾经使用一个控件来控制要启动我的应用程序的作业。单例负责序列化作业,并将其状态显示在程序中感兴趣的任何其他部分。在这种情况下,您可以将单例视为运行在应用程序内部的“服务器”类。


3
记录器通常是单例记录器,因此不必传递记录对象。日志流的任何体面实现都将确保不可能进行并发写入,无论是否为Singleton。
metao

10

当管理对整个应用程序共享的资源的访问时,应使用单例,这可能会破坏具有相同类的多个实例。确保对共享资源线程的访问安全是一个很好的例子,说明了这种模式至关重要的地方。

使用Singletons时,应确保不会意外隐藏依赖项。理想情况下,在执行应用程序的初始化代码(对于C#可执行文件为static void Main(),对于Java可执行文件为static void main())的执行期间设置单例(与应用程序中的大多数静态变量一样),然后将其传递给实例化的所有其他需要它的类。这有助于您保持可测试性。


8

我认为单例使用可以认为与数据库中的多对一关系相同。如果您的代码中有许多不同的部分需要使用一个对象的单个实例,那么使用单例才有意义。


6

可以在Test :: Builder中找到一个单例的实际示例,该类几乎支持每个现代Perl测试模块。Test :: Builder单例存储和代理测试过程的状态和历史记录(历史测试结果,计算运行的测试次数)以及测试输出的位置。这些都是协调由不同作者编写的多个测试模块以在单个测试脚本中一起工作所必需的。

Test :: Builder单例的历史具有教育意义。调用new()总是给您相同的对象。首先,所有数据都存储为类变量,而对象本身没有任何内容。这一直有效,直到我想自己测试Test :: Builder为止。然后,我需要两个Test :: Builder对象,一个设置为虚拟对象,以捕获并测试其行为和输出,另一个是真正的测试对象。那时,Test :: Builder被重构为一个真实的对象。单例对象存储为类数据,new()并将始终返回它。 create()已添加以制作新对象并启用测试。

当前,用户希望在自己的模块中更改Test :: Builder的某些行为,而让其他行为搁浅,而测试历史记录在所有测试模块中仍然是相同的。现在正在发生的事情是将整体的Test :: Builder对象分解成较小的片段(历史记录,输出,格式...),而Test :: Builder实例将它们收集在一起。现在Test :: Builder不再必须是单例。它的组成部分就像历史一样。这将单身人士的僵化必要性推低了一个层次。它为用户提供了更大的灵活性来进行混搭。较小的单例对象现在可以只存储数据,其包含的对象决定如何使用它。它甚至允许通过使用Test :: Builder历史记录和输出单例来与非Test :: Builder类一起玩。

似乎在数据的协调与行为的灵活性之间存在推拉作用,这可以通过将单例放在仅具有最小行为量的共享数据周围以确保数据完整性来缓解。


5

当您从数据库或文件中加载配置属性对象时,将其作为单例是有帮助的。没有理由继续读取服务器运行时不会改变的静态数据。


2
为什么不只加载一次数据并根据需要传递配置对象?
lagweezle

路过的是什么???如果我必须传递我需要的每个对象,我将拥有带有20个参数的构造函数……
Enerccio

@Enerccio如果您有不依赖封装就依赖20个其他对象的对象,那么您已经遇到了主要的设计问题。
光谱

@spectras我吗?如果实现gui对话框,我将需要:存储库,本地化,会话数据,应用程序数据,小部件父项,客户端数据,权限管理器,以及可能更多。当然可以合并一些,但是为什么呢?我个人使用spring和aspect将所有这些依赖关系自动装配到widget类中,从而使所有组件解耦。
Enerccio

如果状态如此多,则可以考虑实现外观,从而为特定上下文提供相关方面的视图。为什么?因为这将允许没有单例或29参数构造函数反模式的干净设计。实际上,您的gui对话框访问所有这些内容实际上是在喊“违反单一责任原则”。
光谱

3

就像每个人都说过的,共享资源-特别是不能处理并发访问的资源。

我看到的一个具体示例是Lucene搜索索引编写器。


1
但是,IndexWriter并不是一个单身汉……
马克

3

实施State模式时,可以使用Singleton(按照GoF书中所示的方式)。这是因为具体的状态类没有自己的状态,而是根据上下文类执行其操作。

您也可以将Abstract Factory设为单例。


我现在在一个项目中正在处理的就是这种情况。我使用状态模式从上下文的方法中删除了重复的条件代码。状态本身没有实例变量。但是,在是否应该使它们单身方面,我处于围栏。每当状态切换时,都会实例化一个新实例。这看起来确实是浪费的,因为实例不可能与另一个实例有所不同(因为没有实例变量)。我试图弄清楚为什么不应该使用它。
kiwicomb123 '17

1
@ kiwicomb123尝试让您setState()负责确定状态创建策略。如果您的编程语言支持模板或泛型,则将有所帮助。可以使用Monostate模式代替Singleton 模式,在该模式下实例化状态对象最终会重用同一全局/静态状态对象。更改状态的语法可以保持不变,因为您的用户不必知道实例化的状态是Monostate。
Emile Cormier

好吧,在我的状态下,我可以将所有方法设为静态,因此,无论何时创建新实例,它都不会具有相同的开销?我有点困惑,我需要阅读有关Monostate模式的信息。
kiwicomb123,2017年

@ kiwicomb123不,Monostate并不是要使所有成员静态。最好您先阅读一下,然后检查SO的相关问题和答案。
Emile Cormier

我觉得这应该有更多的选票。抽象工厂已经足够普遍,并且由于工厂是无状态的,稳定于无状态的,并且不能用未重写的静态方法(在Java中)实现,因此可以使用单例。
DPM

3

共享资源。特别是在PHP中,有一个数据库类,一个模板类和一个全局变量仓库类。所有这些都必须由整个代码中使用的所有模块/类共享。

这是一种真正的对象用法->模板类包含正在构建的页面模板,并且通过添加到页面输出的模块对其进行成形,添加,更改。它必须作为一个实例保存,这样才能发生,数据库也是如此。使用共享的数据库单例,所有模块的类都可以访问查询并获得查询,而无需重新运行它们。

全局变量仓库单例为您提供了全局,可靠且易于使用的变量仓库。它使您的代码更加整洁。想象一下,将所有配置值都放在一个数组中,例如:

$gb->config['hostname']

或将所有语言值都放在一个数组中,例如:

$gb->lang['ENTER_USER']

在运行页面代码的最后,您得到了一个已经成熟的代码:

$template

Singleton,$gb具有可替换的lang数组的Singleton,所有输出均已加载并准备就绪。您只需将它们替换为现在存在于成熟模板对象的页面值中的键,然后将其提供给用户即可。

这样做的最大好处是,您可以对任何内容进行任何后处理。您可以将所有语言值通过管道传输到Google翻译或其他翻译服务,然后将其取回,然后将其替换为它们的位置(例如,已翻译)。或者,您可以根据需要替换页面结构或内容字符串。


21
您可能希望将答案分成多个段落,并屏蔽代码段以提高可读性。
贾斯汀2010年

1

将特定的基础结构问题配置为单例或全局变量可能非常实用。我最喜欢的示例是依赖注入框架,该框架利用单例作为框架的连接点。

在这种情况下,您将依赖于基础结构来简化库的使用并避免不必要的复杂性。


0

在处理可插拔模块时,我将其用于封装命令行参数的对象。主程序不知道要加载的模块的命令行参数是什么(甚至不总是知道要加载的模块)。例如,主加载A本身不需要任何参数(所以为什么它应该使用额外的指针/引用/任何我不确定的东西-看起来像污染),然后加载模块X,Y和Z。两个其中的X和Z表示需要(或接受)参数,因此他们回叫到命令行单例以告诉它要接受哪些参数,然后在运行时回叫以查明用户是否实际指定了任何参数。其中。

在许多方面,如果您每个查询仅使用一个进程,则用于处理CGI参数的单例将以类似的方式工作(其他mod_ *方法不这样做,因此在那里很糟糕-因此,说您不应如果您移植到mod_perl或其他任何世界,请在mod_cgi世界中使用单例。


-1

也许是一个带有代码的例子。

在这里,ConcreteRegistry是扑克游戏中的一个单例,它允许整个包树的行为访问游戏的几个核心界面(即,模型,视图,控制器,环境等的外观):

http://www.edmundkirwan.com/servlet/fractal/cs1/frac-cs40.html

埃德


1
链接现在已断开,但是如果您要在一个视图中注册视图信息(将在整个应用程序中访问该视图信息),则会丢失MVC的要点。视图由使用模型的控制器更新(并与之通信)。听起来很像,这可能是对Singleton的误用,因此必须进行重构。
drharris 2010年

-9

1-对第一个答案的评论:

我不同意静态Logger类。这对于一个实现来说可能是实际的,但是对于单元测试来说是不可替代的。静态类不能用测试双精度代替。如果您不进行单元测试,则不会在这里看不到问题。

2-我尽量不要手工创建一个单例。我只是使用构造函数创建一个简单的对象,该构造函数使我可以将协作者注入该对象。如果需要单身人士,则可以使用依赖检查框架(Spring.NET,Unity for .NET,Spring for Java)或其他一些框架。


11
您应该通过单击答案底部的链接直接对答案发表评论;这样读起来容易得多。另外,您在顶部看到的答案可能不是第一个。答案一直都在重新排序。
罗斯,

为什么要进行单元测试日志记录?
Enerccio '18

“静态Logger类”和静态Logger 实例之间存在巨大差异。单例模式没有说“使您的类成为静态”,而是说使对对象实例的访问成为静态。因此,例如,ILogger logger = Logger.SingleInstance();在此方法为静态的情况下,它返回一个静态存储的ILogger实例。您使用了“依赖项注入框架”的示例。几乎所有的DI容器都是单例;它们的配置是静态定义的,最终可以从单个服务提供者接口访问或存储在单个服务提供者接口中。
乔恩·戴维斯
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.