设计模式:何时应使用单例模式?

565

被美化的全局变量 - 变成了一个受尊崇的全局类。有人说这破坏了面向对象的设计。

给我一些场景,除了传统的日志记录器之外,在这些场景下使用单例模式是有意义的。


4
学习了 Erlang 之后,我更倾向于采用它的方式,也就是不可变性和消息传递。 - Setori
233
这个问题有哪些方面不够建设性?我看到下面有建设性的回答。 - mk12
3
依赖注入框架是一个非常复杂的单例对象,它用于提供其他对象。 - Ian Ringrose
1
Singleton 可以作为其他对象实例之间的管理对象,因此应该只有一个 Singleton 实例,每个其他实例都应通过 Singleton 实例进行通信。 - Levent Divilioglu
我有一个旁问:任何Singleton实现也可以使用“静态”类(带有“工厂”/“init”方法)来实现 - 而不实际创建类的实例(你可以说静态类是一种Singleton实现,但...)- 为什么应该使用实际的Singleton(确保其单个的单个类实例)而不是静态类?我能想到的唯一原因可能是为了“语义”,但即使在这种情况下,Singleton用例也不需要“类->实例”关系来定义...那么...为什么? - Yuval A.
我在DatabaseHelper类中使用它。这个类包含了所有的查询等内容,然后调用Database Helper.getInstance(),这样就不会创建多个无用的对象。 - Yugraaj Sandhu
24个回答

409
在寻求真相的过程中,我发现实际上使用单例模式的"可接受"理由非常少。
经常在互联网上出现的一个原因是“日志记录”类(正如你所提到的)。在这种情况下,可以使用单例代替一个类的单个实例,因为日志记录类通常需要被项目中的每个类反复无常地使用。如果每个类都使用这个日志记录类,那么依赖注入将变得麻烦。
日志记录是一种“可接受”的单例的具体示例,因为它不会影响代码的执行。禁用日志记录,代码执行仍然相同。启用它,也是如此。Misko在Singletons的根本原因中这样表达,“这里的信息流只有一个方向:从您的应用程序进入记录器。即使记录器是全局状态,但由于没有任何信息从记录器流向您的应用程序,记录器是可以接受的。”
我相信还有其他有效的理由。Alex Miller在"我讨厌的设计模式"中,也谈到了服务定位器和客户端UI也可能是可能的“可接受”选择。

阅读更多关于单例模式,我爱你,但你让我失望。


3
我猜测这个链接就是您需要的内容。 - Attacktive
3
为什么不能只使用全局对象?为什么一定要使用单例模式? - Shoe
1
我认为为日志工具使用静态方法? - Skynet
3
单例模式在需要管理资源时是最好的选择。例如,HTTP连接。您不希望向单个客户端建立100万个HTTP客户端,这非常浪费资源且速度缓慢。因此,使用连接池化的HTTP客户端的单例模式将更快且更友好地利用资源。 - Cogman
17
我知道这是一个旧问题,而且答案中的信息很棒。然而,我不太明白为什么这是被接受的答案,因为原帖明确指出:“除了良好的记录器之外,请给我一些其他场景,在这些场景中使用单例是有意义的。” - Francisco C.
显示剩余9条评论

151

Singleton模式需要满足三个要求:

  • 控制共享资源的并发访问。
  • 来自系统中多个不同部分的访问请求该资源。
  • 只能存在一个对象。

如果你的Singleton只满足其中一到两个要求,重新设计通常是正确的选择。

例如,打印机队列很少会从多个地方调用(只有Print菜单),所以可以使用互斥锁解决并发访问问题。

一个简单的日志记录器可能是可行的Singleton的最明显例子,但在更复杂的日志记录方案中这种情况可能会发生变化。


3
我不同意第二点。第三点并不是一个真正的理由(仅仅因为你可以做到,并不意味着你应该这样做),而第一点是一个好观点,但我仍然看不出来它的用处。假设共享资源是磁盘驱动器或数据库缓存。你可以添加另一个驱动器或者有一个专注于其他事情的数据库缓存(例如一个线程的专用表缓存,而另一个则更通用)。 - user34537
21
我认为你漏掉了“候选人”这个词。一个单例候选人必须满足三个要求;仅仅因为某个东西符合这些要求,并不意味着它就应该成为单例。可能还有其他设计因素 :) - metao
打印池不符合标准。您可能需要一个测试打印池,它实际上并不打印,用于测试。 - user253751
假设你有一个用不可变树结构表示的世界数据,而你想要协调更改以管理并发。那么这个树是否适合作为单例模式的候选对象? - DavidY

62

读取只应在启动时读取的配置文件并将其封装在单例中。


8
类似于 .NET 中的 Properties.Settings.Default - Nick Bedford
11
@Paul,“无单例派”认为配置对象应该仅在需要时传递给函数,而不是使其全局可访问(也称为单例)。 - Pacerier
2
不同意。如果配置移动到数据库中,一切都会出问题。如果配置的路径取决于单例之外的任何内容,这些内容也需要是静态的。 - rr-
3
@PaulCroarkin,您能详细阐述并解释这对我们有什么好处吗? - Alex
2
如果配置移到数据库中,仍然可以将其封装在配置对象中,并将其传递给需要它的函数。(附言:我不属于“无单例”阵营)。 - Will Sheppard
配置(在我们谈论的意义上)应始终为只读,即使您可以通过正确设置权限来使用数据库实现它。在各个地方传递配置最终会导致有人试图在运行时进行调整,不,谢谢,但不要这样做... - jave.web

50

当您需要管理共享资源时,可以使用单例模式。例如,打印机缓冲池。您的应用程序应该只有一个缓冲池实例,以避免对同一资源的冲突请求。

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


41
我听说过这个打印机排队程序的例子,但我觉得有点无聊。谁说我不能有多个排队程序?那么什么是打印机排队程序呢?如果我有不同类型的打印机,它们不能冲突或使用不同的驱动程序怎么办? - 1800 INFORMATION
8
这只是一个例子……对于任何人使用作为例子的情况,您都可以找到一种替代设计,使得该例子变得无用。假设卷轴管理多个组件共享的单个资源。它有效运行。 - Vincent Ramdhanie
4
这是四人帮的经典例子。我认为一个拥有真实试验用例的答案会更有用。我的意思是,您实际上感觉到Singleton是最佳解决方案的情况。 - Andrei Vajna II
2
打印机的缓冲池是什么鬼? - RayLoveless
1
@1800INFORMATION 那么,这么多年过去了,打印机的打印队列是什么? - Sajuuk
显示剩余3条评论

28

只读单例模式可以用于存储某些全局状态(如用户语言、帮助文件路径、应用程序路径),这是合理的。但要注意,不要使用单例模式来控制业务逻辑——单例几乎总是会成为多个实例。


6
假设只有一个用户可以使用系统,那么用户语言只能是单一的。 - Samuel Åslund
3
…而且一个用户只会说一种语言。 - spectras
4
@SamuelÅslund 如果这是一个桌面应用程序,那么这个推断是合理的。 - user253751
1
@user253751 是的,直到它突然不再是了,将Java语言中的单例转换为支持国际化网站需要大量的工作。我发现在参数中使用单例通常是一个合理的折衷方案,通过在调用者中检索单例实例,使用它的函数可以被隔离地测试和重复使用,而明显的全局设置不需要在长调用堆栈中传递。许多语言支持默认参数,可用于避免重复。 - Samuel Åslund
@spectras 虽然我同意,这在操作系统等场景下是一个常见情况,你不希望屏幕上到处都是混合语言,即使用户会说多种语言。 - jave.web
显示剩余3条评论

24

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

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


3
一个数据库连接生成器是不是一个工厂模式的例子? - Ken
4
@Ken 在大多数情况下,你都希望将该工厂设计为单例模式。 - Chris Marisic
3
@Federico,“非单例派”认为这些数据库连接应该仅在需要时通过函数传递,而不是使它们全局可访问(也称为单例)。 - Pacerier
3
不一定需要使用单例模式。可以注入该对象。 - Nestor Ledon
@NestorLedon,这实际上取决于你使用它的频率,两种方法都可以实现,但如果你在应用程序的99%中使用某些东西,则依赖注入可能不是正确的方式。另一方面,如果你只是偶尔使用它,但仍然应该是“相同”的“东西”,那么依赖注入可能是正确的方式 :) - jave.web
@java.web 不要啊!一般来说,它所涉及的文件越多,注入的可能性就越大。这样可以在底层更换技术,非常适合存储依赖项。如果单例模式使用接口进行抽象,那么至少可以在那里更新实例化,我会给你留点余地。 - Nestor Ledon

16

单例模式应该用于管理整个应用程序共享的资源访问,并且可能有多个相同类的实例可能会破坏性。确保对共享资源的访问是线程安全的是这种模式非常重要的一个例子。

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


13

使用单例的一种方式是,覆盖必须有单个“代理”控制对资源访问的实例。单例在记录器中非常好用,因为它们代理对只能被独占写入的文件的访问。对于像记录日志这样的内容,它们提供了一种将写入到类似日志文件的内容抽象化的方式 - 您可以将缓存机制包装到您的单例中等等...

此外,考虑一种情况,您需要一个应用程序具有多个窗口/线程/等,但需要单个通信点。我曾经使用单例来控制我想让应用程序启动的作业。 单例负责序列化作业并将其状态显示给程序中任何其他感兴趣的部分。在这种情况下,您可以将单例看作是在应用程序内部运行的“服务器”类...希望这能帮到您。


4
记录器通常是单例模式,这样就不需要传递记录对象。任何良好的日志流实现都会确保并发写入是不可能的,无论它是单例模式还是其他模式。 - metao

12

我认为单例模式的使用可以被看作是数据库中的一对多关系。如果您的代码中有许多不同部分需要使用同一个对象实例,那么使用单例模式就是明智的选择。


8
当你加载一个配置属性对象时, 无论是从数据库或文件中,将其作为单例会很有帮助; 在服务器运行期间不需要重复读取静态数据。

5
为什么不直接加载数据并根据需要传递配置对象? - lagweezle
1
传递对象是怎么回事?如果我必须传递每个需要的对象,那我将会有20个参数的构造函数... - Enerccio
如果您有依赖于其他20个对象且未进行封装的对象,则已经存在重大的设计问题。 - spectras
@spectras 我需要吗?如果我实现GUI对话框,我将需要:存储库、本地化、会话数据、应用程序数据、小部件父级、客户端数据、权限管理器等等。当然,你可以聚合一些东西,但为什么呢?在个人方面,我使用Spring和Aspects来自动将所有这些依赖项装配到小部件类中,并使它们解耦。 - Enerccio
如果您有这么多状态,可以考虑实现一个外观模式,为特定上下文提供相关方面的视图。为什么?因为它将允许干净的设计,而不会出现单例或29个参数构造函数反模式的情况。实际上,您的GUI对话框访问所有这些内容的事实表明“单一责任原则”的违反。 - spectras

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接