我有一个应用程序,其中有几个类用于存储应用程序范围内的设置(资源位置、用户设置等)。目前,这些类只是充满了静态字段和方法,但我从未实例化它们。
有人建议我将它们制作为单例模式,有什么利弊呢?
我认为单例模式是最不适当的设计模式之一。在我12年的软件开发经验中,我可能只见过5个合适的例子。
我曾经参与一个项目,其中我们有一个系统监控服务,用一个System
类(不要与Java内置的System
类混淆)来建模我们的系统,该类包含一个Subsystem
列表,每个子系统又包含一个Component
列表,以此类推。设计师将System
设置为单例。我问道:“为什么要这样做?”回答是:“嗯,只有一个系统。”“我知道,但是,你为什么要将它设置为单例?难道不能实例化一个普通类的一个实例并将其传递给需要它的类吗?”“到处调用getInstance()
比传递它更容易。”“哦......”
这个例子很典型:单例经常被误用作访问类的单个实例的便捷方式,而不是出于技术原因强制使用唯一实例。但这是有代价的。当一个类依赖于getInstance()
时,它永远绑定到单例实现。这使得它更难测试、重用和配置。它违反了我遵循的一个基本规则,可能在某些设计原则文章中有一个常见的名称:类不应该知道如何实例化它们的依赖项。为什么?因为它将类硬编码在一起。当一个类调用构造函数时,它绑定到一个实现。getInstance()
也不例外。更好的替代方案是将一个接口传递给类,其他东西可以执行构造函数/getInstance()
/工厂调用。这就是依赖注入框架(如Spring)发挥作用的地方,尽管它们不是必需的(只是非常好用)。
何时使用单例模式是合适的?只有在实例化超过一个对象会破坏应用程序的情况下才使用。这里不是指在太阳系应用程序中实例化两个地球 - 那只是一个错误。我是说当存在一些基础硬件或软件资源,如果调用/分配/实例化多次将炸毁应用程序时。即使在这种情况下,使用单例的类也不应该知道它是一个单例。应该只有一个调用getInstance()
返回一个接口,然后将其传递给需要它的类的构造函数/设置器。我想另一种说法是你应该因为它的“唯一性”而使用单例,而不是因为它的“全局可访问性”。
顺便说一下,在我提到System
是一个单例的那个项目中......嗯,整个代码库都有System.getInstance()
,以及其他一些不适当的单例。一年后,一些新的需求出现了:“我们将我们的系统部署到多个站点,并希望系统监控服务能够监控每个实例。” 每个实例...... 嗯......getInstance()
不起作用了 :-)
《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,它们具有多个相关方法,这些方法作用于数组或集合。
Singleton会给你一个对象引用,你可以在整个应用程序中使用它...如果你想要对象和/或多态性,你将使用singleton...
我认为你不需要创建单例类。 只需将该类构造函数设置为私有。 就像Java中的Math类一样。
public final class Math {
/**
* Don't let anyone instantiate this class.
*/
private Math() {}
//static fields and methods
}
单例模式经常出现在像您所描述的情况中 - 您有一些需要存储在某个地方的全局数据,并且您只需要一个,最好确保您只能拥有一个。
您可以做的最简单的事情是使用静态变量:
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或其他管理资源加载的框架构建的重要原因。
所以基本上:
静态变量
单例模式可以解决其中一些问题,依赖注入框架可以解决和简化单例加载涉及的一些问题。
在编程中,你应该使用单例模式进行模块化。
想象以下实体以单例模式存在:
Printer prt;
HTTPInfo httpInfo;
PageConfig pgCfg;
ConnectionPool cxPool;
案例1。 想象一下,如果你没有这样做,而是用一个单一的类来保存所有的静态字段/方法。那么你将有一个巨大的静态池需要处理。
案例2。 在您当前的情况下,您将它们分成了适当的类,但作为静态引用。然后会有太多的噪音,因为现在每个静态属性都对您可用。如果您不需要这些信息,特别是当有很多静态信息时,那么您应该限制代码的当前范围,以防止看到不需要的信息。
避免数据混乱有助于维护和确保依赖关系受到限制。在编码时,对我当前范围内可用或不可用的内容有所了解,有助于我更有效地编写代码。
案例3 资源标识。
单例模式可以轻松扩展资源。假设现在您只需要处理一个数据库,因此您将所有设置都放在MyConnection类的静态属性中。如果某个时候您需要连接到多个数据库怎么办?如果您将连接信息编码为单例模式,则代码增强将更加简单。
如果你使用静态方法,那么你的代码将会出现问题,因为你将无法像LaserPrinter.isAlive一样访问BasicPrinter.isAlive。那么你的单一代码将无法管理不同类型的打印机,除非你放置冗余代码。
如果你在Java中编码,你仍然可以实例化一个完全静态的内容类,并使用实例引用来访问其静态属性。如果有人这样做,为什么不把它变成单例呢?
当然,扩展单例类还有其他问题,但是有简单的方法来缓解这些问题。
案例5 避免信息炫耀。 只有极少数的信息需要像最大和最小整数一样被全局共享。为什么Printer.isAlive可以进行炫耀呢?只有非常受限制的信息才应该被允许进行炫耀。
有一句话:思考全局,行动本地。同样,程序员应该使用单例来思考全局,但在本地行动。
如果您从未需要实例化它们,我不认为单例模式有意义。好吧,我应该修正一下 - 如果这些类无法被实例化,那么将它们与单例进行比较就没有意义 - 单例模式将实例化限制为一个对象,并将其与无法实例化的东西进行比较是没有意义的。
我发现单例的主要用途通常涉及具有静态方法的类,这些方法在可能准备好环境后会实例化自己的实例。通过使用私有构造函数并覆盖Object.clone()
以抛出CloneNotSupportedException
,没有其他类可以创建它的新实例,或者如果它们曾经传递了它的一个实例,它们无法对它进行克隆。
我想我可以说,如果您的应用程序设置是一个从未实例化过的类的一部分,那么说“它应该/不应该是单例”是不相关的。
Object.clone()
方法。这种方法是使用一个私有内部类来持有单例类的实例。当你调用getInstance()
方法时,它会指向内部实例。 - JBirch由于您的类持有全局设置,单例模式的一个优点是您可以更多地控制单例的创建。您可以在对象创建期间读取配置文件。
在其他情况下,如果方法是静态的,则不会像Java的Math类那样受益,该类仅具有静态成员。
单例模式的一个更明显的需求是当您将工厂实现为单例时,因为您可以交换此工厂的不同实现。
编写更智能的代码,而不是更艰难的。
《Effective Java》中提到:
单例通常代表一些在系统中本质上是唯一的组件,比如视频显示器或文件系统。
所以,如果你的组件需要在整个应用程序中保持单一实例,并且具有一些状态,那么将其设计为单例是有意义的。
(以上内容借鉴自naikus)
在许多情况下,可以通过一个“工具类”来处理上述情况,其中所有方法都是静态的。但有时候还是更喜欢使用单例。
推荐使用单例而不是静态工具类的主要原因是在设置它时存在一定的成本。例如,如果你的类代表文件系统,它将需要一些初始化操作,这可以放在单例的构造函数中,但对于静态工具类,则必须在静态初始化器中调用。如果你的应用程序的某些执行过程可能永远不会访问文件系统,那么使用静态工具类仍然需要支付初始化的成本,即使你并不需要它。而对于单例,如果你从未需要实例化它,就不会调用初始化代码。
话虽如此,单例几乎肯定是最被滥用的设计模式之一。