单例模式的定义是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点
特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
单例模式是一种常用的模式,有一个对象我们往往只需要一个,如线程池、全局缓存、浏览器中的window对象等。
优点:
最明显的优点就是,只有一个实例,节约系统资源。
缺点
-
对测试不友好。
如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。
-
扩展性差,难以继承。
实际项目中,遇到需求变更往往需要直接在原始类中修改。
说到这里,我们该如何来创建单例呢?往往有两种方式,饿汉式和懒汉式。
饿汉式-怕挨饿,先实例
class UserApi {
private static _instance = new UserApi();
private constructor() {} // private 保证了无法在外部使用 new XXX() 来创建对象
public static getInstance() {
return UserApi._instance;
}
}
UserApi.getInstance();
懒汉式-用到再实例
class UserApi {
private static _instance: UserApi;
private constructor() {}
public static getInstance() {
if (!UserApi._instance) {
UserApi._instance = new UserApi();
}
return UserApi._instance;
}
}
UserApi.getInstance();
懒汉式和饿汉式的区别:
-
线程安全
饿汉式是线程安全的,懒汉式则不是。由于类的实例化也是需要时间的,当多个线程同时需要初始化一个类,第一个线程走到这行代码时
UserApi._instance = new UserApi();开始初始化实例,但还未赋值,第二个线程走到if (!UserApi._instance)的判断时,发现还没有实例,也会实例化一个,这样系统中就会出现2个实例,可能就会影响到系统的正常运行。 -
加载性能
由于饿汉式是系统初始化就会自动实例化,所以会导致系统初始化相对慢一些,实际用到的时候获取实例会很快。而懒汉式则是在实际使用到的时候才初始化,不会拖累系统的初始化,但实际用的时候速度会慢一点。
当然懒汉式并非不能做到做到线程安全,在一些语言中可以使用 同步锁 的方式来保证类不会被多次实例化。
tips:在旧系统中,如何在尽可能小的改动下,将一个类改造成单例?
答:我们可以使用代理类 、工厂模式等来实现
// 旧系统
class UserApi {
constructor(){}
}
我们可以不去修改旧的类,而是添加一个代理类,负责管理类的实例化。把创建实例和管理单例的职责分给不同的类,这也符合 单一职责 的原则。
class UserApiProxy {
private static _instance: UserApi;
private constructor() {}
public static getInstance() {
if (!UserApiProxy._instance) {
UserApiProxy._instance = new UserApi();
}
return UserApiProxy._instance;
}
}
UserApiProxy.getInstance() // 获取的就是UserApi的单例
有限多例
上面我们介绍了一个单例模式,笔者也简单介绍下单例模式的扩展——有限多例。
有限多例,故名思义,可以有多个实例,但又不是无上限的多例。《设计模式之禅》里喜欢用皇帝和大臣的例子来讲解这个模式。
通常,同一时期只有一个皇帝,但在个别情况下,同一时期还有两位皇帝!在历史上的明朝时期,就存在过一朝同时有两位皇帝的情况,也就是明英宗朱祁镇和明代宗朱祁钰时期。
这里我就不写代码实现了,有兴趣的可以自己写写。有限多例的情况在这个场景下的表现,大臣面见皇帝,可能是明英宗,也可能是明代宗。
有限多例的场景应用常见于数据库连接池等。不过,笔者实际项目中好像只在使用puppeteer截图时,使用该模式保证最多产生2个client实例,避免多个client同时运行,cpu负载过高,导致程序崩溃。