1 意图
单例模式可以保证一个类只有一个实例,提供一个访问该实例的全局节点。
2 问题
单例模式同时解决了两个问题,所以违反了单一职责原则。
- 保证一个类只有一个实例。
为什么要控制一个类的实例数量?最常见的原因是控制某些共享资源(如数据库或者文件)的访问权限。
其运行方式是:如果创建了一个对象,稍后决定再创建一个对象时,会得到先前创建的对象,而不是一个新的对象。
注意:普通构造函数无法实现上述行为,构造函数的设计决定了它必须总是返回一个新的对象。
- 为类的单一实例提供一个全局访问节点。
全局变量使用十分方便,但是也非常不安全:任何代码都有可能覆盖掉全局变量的值,引发程序崩溃。
单例模式允许程序在任何地方访问特定对象,同时可以保护该实例不被其他代码覆盖。
此外,不希望解决同一个问题的代码分散在程序各处。更好的方式是将其放在同一个类中,特别是其他代码已经依赖这个类时更应该如此。3 解决方案
单例模式包含两个步骤:
- 将默认构造函数设置为私有,防止其他对象使用
new
运算符创建实例 新建一个静态构建方法作为构造函数。该函数会调用私有构造函数来创建对象,将其保存在一个静态成员变量中。此后所有对该函数的调用都将返回这个缓存对象。
4 结构
单例(Singleton)类声明了一个名为getInstace()
的静态方法来返回所属类的一个相同实例。单例构造函数必须对客户端代码隐藏,调用getInstance()
是获取单例对象的唯一方式。5 伪代码
``java // 数据库类会对
getInstance(获取实例)`方法进行定义以让客户端在程序各处 // 都能访问相同的数据库连接实例。 class Database is // 保存单例实例的成员变量必须被声明为静态类型。 private static field instance: Database// 单例的构造函数必须永远是私有类型,以防止使用
new
运算符直接调用构 // 造方法。 private constructor Database() is// 部分初始化代码(例如到数据库服务器的实际连接)。
// ...
// 用于控制对单例实例的访问权限的静态方法。 public static method getInstance() is
if (Database.instance == null) then
acquireThreadLock() and then
// 确保在该线程等待解锁时,其他线程没有初始化该实例。
if (Database.instance == null) then
Database.instance = new Database()
return Database.instance
// 最后,任何单例都必须定义一些可在其实例上执行的业务逻辑。 public method query(sql) is
// 比如应用的所有数据库查询请求都需要通过该方法进行。因此,你可以
// 在这里添加限流或缓冲逻辑。
// ...
class Application is
method main() is
Database foo = Database.getInstance()
foo.query(“SELECT …”)
// …
Database bar = Database.getInstance()
bar.query(“SELECT …”)
// 变量 bar
和 foo
中将包含同一个对象。
<a name="febb7d8d"></a>
# 6 应用场景
- **如果某个类对所有客户端只需要有一个可用的实例,可以使用单例模式。**<br />单例模式禁止通过特殊构建方法以外的任何方式来创建类的对象。该特殊方法可以创建新对象,如果该对象已经创建,则返回已有的对象。
- **如果需要更加严格地控制全局变量,可以使用单例模式。**<br />单例模式与全局变量不同,它保证类只存在一个实例。除了类自身外,无法通过任何方式替换缓存的实例。<br />可以随时调整限制,设置生成实例的数量,只需要修改`getInstance()`方法。
<a name="4170e445"></a>
# 7 实现方式
1. 在类中添加私有静态成员变量用于保存单例实例。
2. 声明一个公有静态构建方法用于获取单例实例。
3. 在静态方法中实现“延迟初始化”。该方法会在首次被调用时创建一个对象,将其存储在静态成员变量中。此后该方法每次被调用时都返回该实例。
4. 将类的构造函数设置为私有。类的静态方法可以调用构造函数,其他对象不能调用。
5. 检查客户端代码,将对单例类构造函数的调用,替换成对其静态构建方法的调用。
<a name="87338c0b"></a>
# 8 优缺点
- 优点
- 可以保证一个类只有一个实例。
- 有一个获取该实例的全局访问节点。
- 仅在首次请求单例对象时进行初始化。
- 缺点
- 违反了单一职责原则,一个模式解决了两个问题(保证单个实例,获取该实例)。
- 可能会掩盖不良设计,如程序组件之间相互了解过多。
- 多线程环境下需要进行特殊处理,避免多个线程多次创建单例对象。
- 客户端代码的单元测试可能会比较困难,因为许多测试框架以基于继承的方式创建模拟对象。单例类的构造方法是私有的,而且绝大部分语言无法重写静态方法,所以需要考虑模拟单例的方法。要么不编写测试代码,要么不使用单例模式。
<a name="2b0a734f"></a>
# 9 与其他模式的关系
- [外观模式](https://refactoringguru.cn/design-patterns/facade)通常可以转换为[单例模式](https://refactoringguru.cn/design-patterns/singleton),因为大部分情况下一个外观对象就足够了。
- 如果可以将所有共享状态简化为一个享元对象,则[享元模式](https://refactoringguru.cn/design-patterns/flyweight)和单例模拟类似了。但两个模式有根本的不同:
- 单例类只有一个单例实体,但是享元类可以有多个实体,各实体的内在状态也可以不同。
- 单例对象是可变的,享元对象是不可变的。
- [抽象工厂模式](https://refactoringguru.cn/design-patterns/abstract-factory)、[生成器模式](https://refactoringguru.cn/design-patterns/builder)、[原型模式](https://refactoringguru.cn/design-patterns/prototype)都可以用单例模式实现。
<a name="8b93ebe6"></a>
# 10 代码示例
<a name="a8920174"></a>
## 10.1 单例类
```go
ackage main
import (
"fmt"
"sync"
)
var lock = &sync.Mutex{}
type single struct {
}
var singleInstance *single
func getInstance() *single {
if singleInstance == nil {
lock.Lock()
defer lock.Unlock()
if singleInstance == nil {
fmt.Println("Creating single instance now.")
singleInstance = &single{}
} else {
fmt.Println("Single instance already created.")
}
} else {
fmt.Println("Single instance already created.")
}
return singleInstance
}
10.2 客户端
package main
import (
"fmt"
)
func main() {
for i := 0; i < 30; i++ {
go getInstance()
}
// Scanln is similar to Scan, but stops scanning at a newline and
// after the final item there must be a newline or EOF.
fmt.Scanln()
}