单例模式的好处是什么?

7

我有一个应用程序,其中有几个类用于存储应用程序范围内的设置(资源位置、用户设置等)。目前,这些类只是充满了静态字段和方法,但我从未实例化它们。

有人建议我将它们制作为单例模式,有什么利弊呢?


不需要,只需使用一个全局变量。如果您不需要多个,请勿创建多个。 - GManNickG
10个回答

12

我认为单例模式是最不适当的设计模式之一。在我12年的软件开发经验中,我可能只见过5个合适的例子。

我曾经参与一个项目,其中我们有一个系统监控服务,用一个System类(不要与Java内置的System类混淆)来建模我们的系统,该类包含一个Subsystem列表,每个子系统又包含一个Component列表,以此类推。设计师将System设置为单例。我问道:“为什么要这样做?”回答是:“嗯,只有一个系统。”“我知道,但是,你为什么要将它设置为单例?难道不能实例化一个普通类的一个实例并将其传递给需要它的类吗?”“到处调用getInstance()比传递它更容易。”“哦......”

这个例子很典型:单例经常被误用作访问类的单个实例的便捷方式,而不是出于技术原因强制使用唯一实例。但这是有代价的。当一个类依赖于getInstance()时,它永远绑定到单例实现。这使得它更难测试、重用和配置。它违反了我遵循的一个基本规则,可能在某些设计原则文章中有一个常见的名称:类不应该知道如何实例化它们的依赖项。为什么?因为它将类硬编码在一起。当一个类调用构造函数时,它绑定到一个实现。getInstance()也不例外。更好的替代方案是将一个接口传递给类,其他东西可以执行构造函数/getInstance()/工厂调用。这就是依赖注入框架(如Spring)发挥作用的地方,尽管它们不是必需的(只是非常好用)。

何时使用单例模式是合适的?只有在实例化超过一个对象会破坏应用程序的情况下才使用。这里不是指在太阳系应用程序中实例化两个地球 - 那只是一个错误。我是说当存在一些基础硬件或软件资源,如果调用/分配/实例化多次将炸毁应用程序时。即使在这种情况下,使用单例的类也不应该知道它是一个单例。应该只有一个调用getInstance()返回一个接口,然后将其传递给需要它的类的构造函数/设置器。我想另一种说法是你应该因为它的“唯一性”而使用单例,而不是因为它的“全局可访问性”。

顺便说一下,在我提到System是一个单例的那个项目中......嗯,整个代码库都有System.getInstance(),以及其他一些不适当的单例。一年后,一些新的需求出现了:“我们将我们的系统部署到多个站点,并希望系统监控服务能够监控每个实例。” 每个实例...... 嗯......getInstance()不起作用了 :-)


1
那么,哪些是五种适当的情况? - gustafc
@gustacf:它们都是C++类,每个类代表一块定制硬件,与硬件紧密耦合,实例化超过一次会导致硬件/软件无法工作。这是老派的东西,但即使如此,多个进程实例也会导致整个系统崩溃。 - SingleShot
1
所以,我猜你对我的问题的答案是“不”。naikus的回答更适合我的关于应用程序设置的具体问题,所以我接受了他的回答(即使你不同意,哈哈)。 - Tony R
阿门!很多年来,每当有人说 Singleton 是反模式时,我都在说同样的话。对我来说,这就像用螺丝刀当锤子,然后说螺丝刀是反工具一样。 - hfontanez

9

《Effective Java》指出:

Singletons typically represent some system component that is intrinsically 
unique, such as a video display or file system.

如果您的组件需要在整个应用程序中具有单一实例并且具有一些状态,那么将其设置为单例是有意义的。

在您的情况下,应用程序的设置是单例的好选择。

另一方面,如果您想要将某些函数组合在一起,例如实用程序类,则该类只能具有静态方法。jdk中的示例包括java.util.Arrays和java.util.Collections,它们具有多个相关方法,这些方法作用于数组或集合。


5
相比静态类,这样做有什么优势? - Borealid
3
您获得了对类的构建/销毁的控制权,如果您维护状态,则这可能非常重要。静态类应该是无状态的,就像实用程序类一样。 - Jake
在大多数情况下,优势在于可读性和面向对象的优雅。类的实例表示实际初始化状态。而静态方法和字段更适合将某些函数分组在一起,并且您无法为特定实例或类型设置它们。请参见我的更新答案。 - naikus
你应该将这些评论融入到答案中。或者我已经创建了一个包含这些评论的答案。请随意投票支持它 :-) - DJClayworth

1

Singleton会给你一个对象引用,你可以在整个应用程序中使用它...如果你想要对象和/或多态性,你将使用singleton...


在您的应用程序中拥有可以在全局使用的对象引用并不一定是一件好事。在面向对象之前,我们称这些为“全局变量”,它们会使维护非常困难。 - DJClayworth

0

我认为你不需要创建单例类。 只需将该类构造函数设置为私有。 就像Java中的Math类一样。

public final class Math {

    /**
     * Don't let anyone instantiate this class.
     */
    private Math() {}

//static fields and methods

}

这不是一个单例类,而是抽象类的使用(无法实例化)。单例类只被实例化一次,并具有静态实例获取器。 - f1sh
是的,我知道什么是单例类。他在问是否应该使用单例模式,而我告诉他不需要使用单例模式。但你应该将类的构造函数设置为私有,以防止意外的类实例化。 - Vivart
Vivart,有些情况下单例是很有用的。请看我的回答。 - DJClayworth

0

单例模式经常出现在像您所描述的情况中 - 您有一些需要存储在某个地方的全局数据,并且您只需要一个,最好确保您只能拥有一个。

您可以做的最简单的事情是使用静态变量:

public class Globals{
   public static final int ACONSTANT=1;

这样做很好,可以确保只有一个实例,并且没有实例化问题。当然,主要缺点是如果将数据嵌入其中,通常会不方便。如果您的字符串实际上是通过从外部资源构建(例如,通过构建某些东西)而构建的,则还会遇到加载问题(原始类型也有一个陷阱 - 如果您的静态 final 是 int,则依赖它的类将其编译为内联,这意味着重新编译常量可能不会替换应用程序中的常量 - 例如,给定 public class B {int i = Globals.ACONSTANT;} 更改 Globals.ACONSTANT 并仅重新编译 Globals 将使 B.i 仍然等于1.)

构建自己的单例是下一个最简单的事情,通常也很好(尽管查找关于单例加载固有问题的讨论,例如双重检查锁定)。这些问题是许多应用程序使用Spring、Guice或其他管理资源加载的框架构建的重要原因。

所以基本上:

静态变量

  • 好的:易于编码,代码清晰简单
  • 不好的:不可配置-必须重新编译才能更改值,如果全局数据需要复杂初始化,则可能无法工作

单例模式可以解决其中一些问题,依赖注入框架可以解决和简化单例加载涉及的一些问题。


0

在编程中,你应该使用单例模式进行模块化。

想象以下实体以单例模式存在:

Printer prt;
HTTPInfo httpInfo;
PageConfig pgCfg;
ConnectionPool cxPool;

案例1。 想象一下,如果你没有这样做,而是用一个单一的类来保存所有的静态字段/方法。那么你将有一个巨大的静态池需要处理。

案例2。 在您当前的情况下,您将它们分成了适当的类,但作为静态引用。然后会有太多的噪音,因为现在每个静态属性都对您可用。如果您不需要这些信息,特别是当有很多静态信息时,那么您应该限制代码的当前范围,以防止看到不需要的信息。

避免数据混乱有助于维护和确保依赖关系受到限制。在编码时,对我当前范围内可用或不可用的内容有所了解,有助于我更有效地编写代码。

案例3 资源标识。
单例模式可以轻松扩展资源。假设现在您只需要处理一个数据库,因此您将所有设置都放在MyConnection类的静态属性中。如果某个时候您需要连接到多个数据库怎么办?如果您将连接信息编码为单例模式,则代码增强将更加简单。

案例4 继承。
单例类允许自己被扩展。如果你有一个资源类,它们可以共享公共代码。假设你有一个可实例化为单例的基本打印机类BasicPrinter,然后你有一个扩展了BasicPrinter的激光打印机类LaserPrinter。

如果你使用静态方法,那么你的代码将会出现问题,因为你将无法像LaserPrinter.isAlive一样访问BasicPrinter.isAlive。那么你的单一代码将无法管理不同类型的打印机,除非你放置冗余代码。

如果你在Java中编码,你仍然可以实例化一个完全静态的内容类,并使用实例引用来访问其静态属性。如果有人这样做,为什么不把它变成单例呢?

当然,扩展单例类还有其他问题,但是有简单的方法来缓解这些问题。

案例5 避免信息炫耀。 只有极少数的信息需要像最大和最小整数一样被全局共享。为什么Printer.isAlive可以进行炫耀呢?只有非常受限制的信息才应该被允许进行炫耀。

有一句话:思考全局,行动本地。同样,程序员应该使用单例来思考全局,但在本地行动。


0

如果您从未需要实例化它们,我不认为单例模式有意义。好吧,我应该修正一下 - 如果这些类无法被实例化,那么将它们与单例进行比较就没有意义 - 单例模式将实例化限制为一个对象,并将其与无法实例化的东西进行比较是没有意义的。

我发现单例的主要用途通常涉及具有静态方法的类,这些方法在可能准备好环境后会实例化自己的实例。通过使用私有构造函数并覆盖Object.clone()以抛出CloneNotSupportedException,没有其他类可以创建它的新实例,或者如果它们曾经传递了它的一个实例,它们无法对它进行克隆。

我想我可以说,如果您的应用程序设置是一个从未实例化过的类的一部分,那么说“它应该/不应该是单例”是不相关的。


实际上,在Java中有一种非常酷的方法可以实现单例模式,而不需要覆盖Object.clone()方法。这种方法是使用一个私有内部类来持有单例类的实例。当你调用getInstance()方法时,它会指向内部实例。 - JBirch

0

由于您的类持有全局设置,单例模式的一个优点是您可以更多地控制单例的创建。您可以在对象创建期间读取配置文件。

在其他情况下,如果方法是静态的,则不会像Java的Math类那样受益,该类仅具有静态成员。

单例模式的一个更明显的需求是当您将工厂实现为单例时,因为您可以交换此工厂的不同实现。


控制创建也有助于测试依赖于该单例的组件。 - Frank Schwieterman

0
有时人们认为属于 Singleton 对象的东西实际上可以成为类的私有成员。而有时它们应该是唯一的全局变量。这取决于设计需要。
如果应该存在一个,并且 只能 存在一个对象实例:使用 Singleton。我的意思是,如果程序应该在存在多个对象时停止。一个很好的例子是,如果您正在设计一个仅支持呈现到单个输出设备的视频游戏。尝试再次打开相同的设备(为硬编码而感到羞愧!)将被禁止。在我看来,这种情况通常意味着您首先不应该使用类。即使 C 也允许您轻松地封装此类问题,而无需制作 Singleton 类,并仍然保持适用于单例的 OO 元素。当您困在像 Java/C# 这样的语言中时,Singleton 模式就是您必须使用的,除非纯静态成员本身就可以解决问题。通过这些方式,您仍然可以模拟其他方式。
如果只是对象之间的接口问题,你可能应该更多地考虑面向对象。这里有另一个例子:假设我们的游戏引擎渲染代码需要与资源和输入管理器进行接口,以便它能够完成工作。你可以将它们制作成单例并执行ResourceManager.getInstance().getResource(name)等操作。或者你可以创建一个应用程序类(例如GameEngine),它具有ResourceManager和InputManager作为私有成员。然后让GameEngine根据需要将它们传递给渲染代码的方法。例如r.render(resourcemanager)。
对于单例——可以轻松地从任何地方访问,就像全局变量一样,但只能有一个副本。
反对单例——许多使用单例的情况可以通过将其封装在父对象中并将成员对象传递给另一个成员对象的方法来解决。
有时候只是使用愚蠢的全局变量是正确的选择。就像使用GOTO或复合(and/or)条件语句而不是使用复制和粘贴写相同的错误处理代码N次一样。

编写更智能的代码,而不是更艰难的。


0

《Effective Java》中提到:

单例通常代表一些在系统中本质上是唯一的组件,比如视频显示器或文件系统。

所以,如果你的组件需要在整个应用程序中保持单一实例,并且具有一些状态,那么将其设计为单例是有意义的。

(以上内容借鉴自naikus)

在许多情况下,可以通过一个“工具类”来处理上述情况,其中所有方法都是静态的。但有时候还是更喜欢使用单例。

推荐使用单例而不是静态工具类的主要原因是在设置它时存在一定的成本。例如,如果你的类代表文件系统,它将需要一些初始化操作,这可以放在单例的构造函数中,但对于静态工具类,则必须在静态初始化器中调用。如果你的应用程序的某些执行过程可能永远不会访问文件系统,那么使用静态工具类仍然需要支付初始化的成本,即使你并不需要它。而对于单例,如果你从未需要实例化它,就不会调用初始化代码。

话虽如此,单例几乎肯定是最被滥用的设计模式之一。


也许我错了,但是我认为单例类在类加载时创建单个实例,静态代码块也会在类加载时运行。因此,在这两种情况下,初始化代码只会运行一次。 - Vivart
2
单例模式不会在类加载时创建实例,而是在第一次请求实例时创建。如果从未请求实例,则初始化代码永远不会运行。 - DJClayworth
@DJClayworth:没错。实例只有在第一次调用getInstance()或者返回实例的方法时才会被创建。 - Izza

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