单例模式
目的
单例是一种创造性的设计模式,它允许您确保一个类只有一个实例同时为这个实例提供一个全局访问点。
问题
单例模式同时解决了两个问题,违反了单责任原则:
- 确保一个类只有一个实例。为什么会有人想要控制一个类有多少个实例?最常见的原因是控制对某些共享资源(例如数据库或文件)的访问。它是这样工作的:假设您创建了一个对象,但过了一段时间后决定创建一个新对象。您将获得已经创建的对象而不是接收一个新的对象。注意这种行为不可能用常规构造函数实现,因为构造函数调用必须按照设计总是返回一个新对象。
- 为该实例提供一个全局访问点。还记得你用来存储一些基本对象的全局变量吗?虽然它们非常方便但也非常不安全,因为任何代码都可能覆盖这些变量的内容,从而导致应用程序崩溃。就像全局变量一样,单例模式允许您从程序中的任何地方访问某个对象。但是它还可以保护该实例不被其他代码覆盖。这个问题还有另一个方面:您不希望解决问题1的代码分散在您的程序中。最好将它放在一个类中,尤其是在其他代码已经依赖于它的情况下。
如今单例模式已经变得如此流行以至于人们可能会称某个类为单例,即使它只解决了列出的其中一个问题。
解决方案
所有单例模式的实现都有这两个共同的步骤:
- 将默认构造函数设为private,以防止其他对象对单例类使用new操作符。
- 创建一个充当构造函数的静态创建方法。在实现中这个方法调用私有构造函数来创建一个对象并将其保存在一个静态字段中。对这个方法的所有后续调用都会返回缓存的对象。
如果您的代码能够访问单例类,那么它就能够调用单例类的静态方法。无论何时调用那个方法总是返回相同的对象。
与真实世界的对比
政府是单例模式的一个很好的例子。一个国家只能有一个官方政府。无论组成政府的个人身份如何,“政府of X”这个头衔都是一个全球性的访问点,可以识别出政府负责人的身份。
结构
- 单例类声明了一个静态方法getInstance,该方法返回它自己类的同一个实例。单例的构造函数应该对客户端代码隐藏。调用getInstance方法应该是获取Singleton对象的唯一方法。
伪码
在本例中数据库连接类充当单例。这个类没有公共构造函数,所以获得它的对象的唯一方法是调用getInstance方法。这个方法缓存第一个创建的对象,并在所有后续调用中返回它。
// The Database class defines the `getInstance` method that lets
// clients access the same instance of a database connection
// throughout the program.
class Database is
// The field for storing the singleton instance should be
// declared static.
private static field instance: Database
// The singleton's constructor should always be private to
// prevent direct construction calls with the `new`
// operator.
private constructor Database() is
// Some initialization code, such as the actual
// connection to a database server.
// ...
// The static method that controls access to the singleton
// instance.
public static method getInstance() is
if (Database.instance == null) then
acquireThreadLock() and then
// Ensure that the instance hasn't yet been
// initialized by another thread while this one
// has been waiting for the lock's release.
if (Database.instance == null) then
Database.instance = new Database()
return Database.instance
// Finally, any singleton should define some business logic
// which can be executed on its instance.
public method query(sql) is
// For instance, all database queries of an app go
// through this method. Therefore, you can place
// throttling or caching logic here.
// ...
class Application is
method main() is
Database foo = Database.getInstance()
foo.query("SELECT ...")
// ...
Database bar = Database.getInstance()
bar.query("SELECT ...")
// The variable `bar` will contain the same object as
// the variable `foo`.
应用
当你的程序中的一个类只有一个对所有客户端可用的实例时,使用单例模式;例如由程序的不同部分共享的单个数据库对象。
单例模式禁用了创建类对象的所有其他方法除了特殊的创建方法。这个方法要么创建一个新对象要么返回一个已经创建的对象。
当你需要对全局变量进行更严格的控制时,使用单例模式。
与全局变量不同单例模式保证一个类只有一个实例。除了单例类本身之外,没有任何东西可以替代缓存的实例。请注意您总是可以调整这个限制并允许创建任意数量的单例实例。惟一需要更改的代码片段是getInstance方法的主体。
如何实现
- 向类中添加一个私有静态字段,用于存储单例实例。
- 声明一个公共静态创建方法来获取单例实例。
- 在静态方法中实现“延迟初始化”。它应该在第一次调用时创建一个新对象并将其放入静态字段中。在所有后续调用中,该方法应该始终返回该实例。
- 将类的构造函数设为private。类的静态方法仍然可以调用构造函数,但不能调用其他对象。
- 仔细检查客户端代码,将所有对单例构造函数的直接调用替换为对其静态创建方法的调用。
正反面
正面因素:
- 你可以保证一个类只有一个实例。
- 你可以获得此实例的全局访问点。
- 单例对象只有在第一次被请求时才会初始化。
反面因素:
- 违反单一责任原则。该模式同时解决了两个问题。
- 单例模式可以掩盖糟糕的设计,例如当程序的组件彼此了解太多时。
- 该模式需要在多线程环境中进行特殊处理,保证多个线程不会多次创建一个单例对象。
- 对单例的客户端代码进行单元测试可能很困难,因为许多测试框架在生成模拟对象时依赖于继承。由于单例类的构造函数是私有的并且在大多数语言中重写静态方法是不可能的,因此您需要想出一种创造性的方法来模拟单例。或者干脆不写测试,或者不要使用Singleton模式。
同其他设计模式的关联
-
外观类通常可以转换为单例类,因为在大多数情况下单个外观对象就足够了。
-
如果您设法将对象的所有共享状态减少到一个轻量化对象,那么轻量级模式将类似于单例模式。但是这些模式之间有两个基本的区别:
- 应该只有一个单例实例,而轻量化类可以有多个具有不同内在状态的实例。
- 单例对象可以是可变的。轻量化对象是不可变的。
- 抽象工厂、构造器和原型都可以实现为单例。