forked from ascoders/weekly
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
115 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# 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 instance = () => { | ||
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)) |