# Singleton（单例模式）

Singleton（单例模式）属于创建型模式，提供一种对象获取方式，保证在一定范围内是唯一的。

**意图：保证一个类仅有一个实例，并提供一个访问它的全局访问点。**

其实单例模式在前端体会的不明显，原因有：

1. 前端代码本身在单机运行，创建的任何变量都是天然分布式的，不需要担心影响另一个用户。
2. 后端代码是一对多的，分辨出哪些资源是请求间共享的，哪些是请求内独有的很重要。

另外我们说到单例，是隐含了一个范围的，指的是在某个范围内单例，比如在一个上下文中，还是一个房间中，还是一个进程，一个线程中单例，不同场景范围会不同。

## 举例子

如果看不懂上面的意图介绍，没有关系，设计模式需要在日常工作里用起来，结合例子可以加深你的理解，下面我准备了三个例子，让你体会什么场景下会用到这种设计模式。

### 多人游戏的共享物品

玩过游戏的同学都知道，我们在每局游戏中使用的公共物品在当前房间中是唯一的，但在游戏房间间却不是唯一的，所以这些公共物品肯定有不同的类去描述，那每局游戏中怎么拿公共物品，可以保证拿到的是当前局内唯一的？

### Redux 数据流

其实前端的 Redux 数据流本身就是单例模式，在一个应用中，数据是唯一的，但可以有不同的 UI 使用这份唯一的数据，甚至把一个表格组件展示在两个不同地方，比如全屏模式，但数据依然是一份，我们没有必要为了全屏展示表格，就让它再发一次取数请求，完全可以和原来的表格共享一份数据。

### 数据库连接池

每个 SQL 查询都依赖数据库连接池，如果每次查询都建立一次数据库连接池，则建立连接的速度会远远慢于 SQL 查询速度，因此你会怎么设计数据库连接池的获取方法？

## 意图解释

单例模式的意图很简单，几乎就是其字面含义：

**意图：保证一个类仅有一个实例，并提供一个访问它的全局访问点。**

对于多人游戏的共享物品，比如一口锅，要保证在一局游戏内唯一，就要提供一种方法访问到唯一实例。

Redux 数据流的 `connect` 装饰器就是全局访问点的一种设计。

数据库连接池可以提前初始化好，并通过固定 API 提供这个唯一实例。

## 结构图

<img width=600 src="https://img.alicdn.com/tfs/TB1qVf20QY2gK0jSZFgXXc5OFXa-1060-342.png">

`Singleton` 是单例模式的接口，客户只能通过其定义的 `instance()` 访问实例，以保证单例。

## 代码例子

下面例子使用 typescript 编写。

```typescript
class Ball {
  private _instance = undefined

  // 构造函数申明为 private，就可以阻止 new Ball() 行为
  private constructor() {}

  public static getInstance = () => {
    if (this._instance === undefined) {
      this._instance = new Ball()
    }

    return this._instance
  }
}

// 使用
const ball = Ball.getInstance()
```

可以仔细想想，为什么这个例子把单例写成了静态方法，而不是一个全局变量？其实全局变量也能解决问题，但由于会污染全局，要尽可能通过模块化方式解决，上面的例子就是一个较好的封装方式。

当然这只是一个最简单的例子，实际上单例模式还有几种模式：

### 饿汉式

初始化时就生成一份实例，这样调用时直接就能获取。

### 懒汉式

就是代码例子中写的，按需实例化，即调用的时候再实例化。

> **要注意，按需不一定是什么好事，如果 New 的成本很高还按需实例化，可能把系统异常的风险留到随机的触发时机，导致难以排查 BUG，另外也会影响第一次实例化时的系统耗时。**

对 JAVA 来说，单例还需要考虑并发性，有 **双重检测、静态内部类、枚举** 等办法解决，这里不具体展开。

## 弊端

单例模式的问题有：

- 对面向对象不太友好。对封装、继承、多态支持不够友好。
- 不利于梳理类之间的依赖关系。毕竟单例是直接调用的，而不是在构造函数申明的，所以要梳理关系要看完每一行代码才能确定。
- 可拓展性不好。万一要支持多例就比较难拓展，比如全局数据流可能因为微前端方案改成多实例、数据库连接池为了分治 SQL 改成多实例，都是有可能的，在系统设计之初就要考虑到未来是否还会保持单例。
- 可测试性不好，因为单例是全局共享的，无法保证测试用例间的隔离。
- 无法使用构造函数传参。

另外单例模式还可以被工厂方法所替代，所以不用特别纠结一种设计模式，可以结合使用，工厂函数也可以内嵌单例模式。

## 总结

单例模式概念、用法都简单，是架构设计常用方案，但要充分理解到单例模式的弊端，防止不恰当的使用。

<img width=400 src="https://img.alicdn.com/tfs/TB15O3YmOpE_u4jSZKbXXbCUVXa-904-224.png">


> 讨论地址是：[精读《设计模式 - Singleton 单例模式》· Issue #278 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/278)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
