创建型模式-单例模式
创建型模式-单例模式
亦称: 单件模式、Singleton
意图
单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
问题
单例模式同时解决了两个问题, 所以违反了_单一职责原则_:
保证一个类只有一个实例。 为什么会有人想要控制一个类所拥有的实例数量? 最常见的原因是控制某些共享资源 (例如数据库或文件) 的访问权限。
它的运作方式是这样的: 如果你创建了一个对象, 同时过一会儿后你决定再创建一个新对象, 此时你会获得之前已创建的对象, 而不是一个新对象。
注意, 普通构造函数无法实现上述行为, 因为构造函数的设计决定了它必须总是返回一个新对象。
为该实例提供一个全局访问节点。 还记得你 (好吧, 其实是我自己) 用过的那些存储重要对象的全局变量吗? 它们在使用上十分方便, 但同时也非常不安全, 因为任何代码都有可能覆盖掉那些变量的内容, 从而引发程序崩溃。
和全局变量一样, 单例模式也允许在程序的任何地方访问特定对象。 但是它可以保护该实例不被其他代码覆盖。
还有一点: 你不会希望解决同一个问题的代码分散在程序各处的。 因此更好的方式是将其放在同一个类中, 特别是当其他代码已经依赖这个类时更应该如此。
如今, 单例模式已经变得非常流行, 以至于人们会将只解决上文描述中任意一个问题的东西称为单例。
解决方案
所有单例的实现都包含以下两个相同的步骤:
- 将默认构造函数设为私有, 防止其他对象使用单例类的
new
运算符。 - 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象, 并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。
如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。
真实世界类比
政府是单例模式的一个很好的示例。 一个国家只有一个官方政府。 不管组成政府的每个人的身份是什么, “某政府” 这一称谓总是鉴别那些掌权者的全局访问节点。
单例模式结构
单例 (Singleton) 类声明了一个名为
getInstance
获取实例的静态方法来返回其所属类的一个相同实例。单例的构造函数必须对客户端 (Client) 代码隐藏。 调用
获取实例
方法必须是获取单例对象的唯一方式。
伪代码
在本例中, 数据库连接类即是一个单例。
该类不提供公有构造函数, 因此获取该对象的唯一方式是调用 获取实例
方法。 该方法将缓存首次生成的对象, 并为所有后续调用返回该对象。
1 | // 数据库类会对`getInstance(获取实例)`方法进行定义以让客户端在程序各处 |
单例模式适合应用场景
如果程序中的某个类对于所有客户端只有一个可用的实例,可以使用单例模式。
单例模式禁止通过除特殊构建方法以外的任何方式来创建自身类的对象。 该方法可以创建一个新对象, 但如果该对象已经被创建, 则返回已有的对象。
如果你需要更加严格地控制全局变量,可以使用单例模式。
单例模式与全局变量不同, 它保证类只存在一个实例。 除了单例类自己以外, 无法通过任何方式替换缓存的实例。
请注意, 你可以随时调整限制并设定生成单例实例的数量, 只需修改 获取实例
方法, 即 getInstance 中的代码即可实现。
实现方式
- 在类中添加一个私有静态成员变量用于保存单例实例。
- 声明一个公有静态构建方法用于获取单例实例。
- 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
- 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。
- 检查客户端代码, 将对单例的构造函数的调用替换为对其静态构建方法的调用。
单例模式优缺点
优点
- 你可以保证一个类只有一个实例。
- 你获得了一个指向该实例的全局访问节点。
- 仅在首次请求单例对象时对其进行初始化。
缺点
- 违反了_单一职责原则_。 该模式同时解决了两个问题。
- 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
- 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
- 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。
与其他模式的关系
- 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。
- 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。
- 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
- 单例对象可以是可变的。 享元对象是不可变的。
- 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。
代码示例
单例是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
单例拥有与全局变量相同的优缺点。 尽管它们非常有用, 但却会破坏代码的模块化特性。
在某些其他上下文中, 你不能使用依赖于单例的类。 你也将必须使用单例类。 绝大多数情况下, 该限制会在创建单元测试时出现。
在 C# 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 C# 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
基础单例
实现一个粗糙的单例非常简单。 你仅需隐藏构造函数并实现一个静态的构建方法即可。
相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。
Program.cs: 概念示例
1 | using System; |
Output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
线程安全单例
为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。
Program.cs: 概念示例
1 | using System; |
Output.txt: 执行结果
1 | FOO |
在 C++ 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 C++ 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
基础单例
实现一个粗糙的单例非常简单。 你仅需隐藏构造函数并实现一个静态的构建方法即可。
相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。
main.cc: 概念示例
1 | /** |
Output.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay! |
线程安全单例
为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。
main.cc: 概念示例
1 | /** |
Output.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay! |
在 Java 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 Java 代码中的使用频率正在逐步减少。
尽管如此, Java 核心程序库中仍有相当多的单例示例:
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
基础单例(单线程)
实现一个粗糙的单例非常简单。 你仅需隐藏构造函数并实现一个静态的构建方法即可。
Singleton.java: 单例
1 | package refactoring_guru.singleton.example.non_thread_safe; |
DemoSingleThread.java: 客户端代码
1 | package refactoring_guru.singleton.example.non_thread_safe; |
OutputDemoSingleThread.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay!) |
基础单例(多线程)
相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。
Singleton.java: 单例
1 | package refactoring_guru.singleton.example.non_thread_safe; |
DemoMultiThread.java: 客户端代码
1 | package refactoring_guru.singleton.example.non_thread_safe; |
OutputDemoMultiThread.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay!) |
采用延迟加载的线程安全单例
为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。
Singleton.java: 单例
1 | package refactoring_guru.singleton.example.thread_safe; |
DemoMultiThread.java: 客户端代码
1 | package refactoring_guru.singleton.example.thread_safe; |
OutputDemoMultiThread.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay!) |
在 PHP 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 PHP 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
概念示例
本例说明了单例设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 PHP 应用案例。
index.php: 概念示例
1 |
|
Output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
真实世界示例
单例模式由于限制了代码复用, 且让单元测试复杂化而名声不佳。 但它在有些情况下仍然非常实用, 特别是在需要控制一些共享资源时十分方便。 例如, 全局日志对象必须对日志文件的访问权限进行控制。 另一个例子: 共享的运行时配置存储。
index.php: 真实世界示例
1 |
|
Output.txt: 执行结果
1 | 2018-06-04: Started! |
在 Python 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 Python 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
基础单例
实现一个粗糙的单例非常简单。 你仅需隐藏构造函数并实现一个静态的构建方法即可。
相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。
main.py: 概念示例
1 | class SingletonMeta(type): |
Output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
线程安全单例
为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。
main.py: 概念示例
1 | from threading import Lock, Thread |
Output.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay!) |
在 Ruby 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 Ruby 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
基础单例
实现一个粗糙的单例非常简单。 你仅需隐藏构造函数并实现一个静态的构建方法即可。
相同的类在多线程环境中会出错。 多线程可能会同时调用构建方法并获取多个单例类的实例。
main.rb: 概念示例
1 | # The Singleton class defines the `instance` method that lets clients access the |
output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
线程安全单例
为了解决这个问题, 你必须在创建首个单例对象时对线程进行同步。
main.rb: 概念示例
1 | # The Singleton class defines the `intance` method that lets clients access the |
output.txt: 执行结果
1 | If you see the same value, then singleton was reused (yay!) |
在 Swift 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 Swift 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
概念示例
本例说明了单例设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
了解该模式的结构后, 你可以更容易地理解下面基于真实世界的 Swift 应用案例。
Example.swift: 概念示例
1 | import XCTest |
Output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
真实世界示例
Example.swift: 真实世界示例
1 | import XCTest |
Output.txt: 执行结果
1 | MessagesListVC starts receive messages |
在 TypeScript 中使用模式
复杂度: ★☆☆
流行度: ★★☆
使用示例: 许多开发者将单例模式视为一种反模式。 因此它在 TypeScript 代码中的使用频率正在逐步减少。
识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。
概念示例
本例说明了单例设计模式的结构并重点回答了下面的问题:
- 它由哪些类组成?
- 这些类扮演了哪些角色?
- 模式中的各个元素会以何种方式相互关联?
index.ts: 概念示例
1 | /** |
Output.txt: 执行结果
1 | Singleton works, both variables contain the same instance. |
概念示例
通常而言, 单例实例会在结构体首次初始化时创建。 为了实现这一操作, 我们在结构体中定义一个 getInstance
获取实例方法。 该方法将负责创建和返回单例实例。 创建后, 每次调用 getInstance
时都会返回相同的单例实例。
协程方面又有什么需要注意的吗? 每当多个协程想要访问实例时, 单例结构体就必须返回相同的实例。 正因如此, 单例设计模式的实施工作很容易出错。 下方的例子表示了创建单例的正确方式。
一些值得注意的地方:
- 最开始时会有
nil
检查, 确保singleInstance
单例实例在最开始时为空。 这是为了防止在每次调用getInstance
方法时都去执行消耗巨大的锁定操作。 如果检查不通过, 则就意味着singleInstance
字段已被填充。 singleInstance
结构体将在锁定期间创建。- 在获取到锁后还会有另一个
nil
检查。 这是为了确保即便是有多个协程绕过了第一次检查, 也只能有一个可以创建单例实例。 否则, 所有协程都会创建自己的单例结构体实例。
single.go: 单例
1 | package main |
main.go: 客户端代码
1 | package main |
output.txt: 执行结果
1 | Creating single instance now. |
另一个例子
init
函数
我们可以在 init
函数中创建单例实例。 这仅适用于实例的早期初始化工作已经确定时。 init
函数仅会在包中的每个文件里调用一次, 所以我们可以确定其只会创建一个实例。
sync.Once
sync.Once
仅会执行一次操作。 可查看下面的代码:
syncOnce.go: 单例
1 | package main |
main.go: 客户端代码
1 | package main |
output.txt: 执行结果
1 | Creating single instance now. |