创建型模式——单例模式

268 阅读4分钟

单例模式的定义是: 保证一个类仅有一个实例,并提供一个访问它的全局访问点

特点:

  1. 单例类只能有一个实例
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

单例模式是一种常用的模式,有一个对象我们往往只需要一个,如线程池、全局缓存、浏览器中的window对象等。

优点:

最明显的优点就是,只有一个实例,节约系统资源。

缺点

  1. 对测试不友好。

    如果单例类依赖比较重的外部资源,比如 DB,我们在写单元测试的时候,希望能通过 mock 的方式将它替换掉。而单例类这种硬编码式的使用方式,导致无法实现 mock 替换。

  2. 扩展性差,难以继承。

    实际项目中,遇到需求变更往往需要直接在原始类中修改。

说到这里,我们该如何来创建单例呢?往往有两种方式,饿汉式和懒汉式。

饿汉式-怕挨饿,先实例

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();

懒汉式和饿汉式的区别:

  1. 线程安全

    饿汉式是线程安全的,懒汉式则不是。由于类的实例化也是需要时间的,当多个线程同时需要初始化一个类,第一个线程走到这行代码时UserApi._instance = new UserApi();开始初始化实例,但还未赋值,第二个线程走到if (!UserApi._instance)的判断时,发现还没有实例,也会实例化一个,这样系统中就会出现2个实例,可能就会影响到系统的正常运行。

  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负载过高,导致程序崩溃。