【设计模式】23 种设计模式
Dandelion 9/7/2022 design-pattern
# 概述
# 基本概念
- SOLID:单一职责原则,开闭原则,里氏代换原则,依赖倒置原则,接口隔离原则,合成复用原则,迪米特法则
- 设计模式分类
- 创建型:单例模式、工厂模式、建造者模式
- 结构型:适配器模式、装饰器模式、代理模式
- 行为型:策略模式、观察者模式(发布订阅模式)、职责链模式、中介者模式
# 单例模式
实现原理:一个类只有一个实例,并提供一个访问它的全局访问点。
代码实现
class Singleton { static _instance = null; static getInstance() { if (!Singleton._instance) { Singleton._instance = new Singleton(); } return Singleton._instance; } } const s1 = Singleton.getInstance(); const s2 = Singleton.getInstance(); console.log(s1 === s2); // true
1
2
3
4
5
6
7
8
9
10
11
12
13应用场景
- Vuex 实现了一个全局的 store 来存储应用的所有状态,store 的实现就是单例模式的典型应用。
- 通过调用 Vue.use 方法,安装 Vuex 插件。
- Vuex 插件本质上是一个对象,内部实现了一个 install 方法,这个方法在插件安装时被调用,从而把 store 注入到 Vue 实例中。
- 通过这种方式,可以保证一个 Vue 实例只会被 install 一次 Vuex 插件,所以每个 Vue 实例只会拥有一个全局的 Store。
- Vuex 实现了一个全局的 store 来存储应用的所有状态,store 的实现就是单例模式的典型应用。
优点:节约资源,保证访问的一致性。
缺点:扩展性不友好,因为单例模式一般自行实例化,没有接口。
# 工厂模式
实现原理:根据不同的参数,返回不同类的实例。将对象的创建与对象的实现分离。
代码实现(Event 类)
class Event { constructor() { this.eventCache = {}; } addEventListener(type, callback, once = false) { if (this.eventCache[type] === undefined) { this.eventCache[type] = []; } if (typeof callback === 'function') { if (!this.eventCache[type].includes(callback)) { this.eventCache.push(callback); } callback.once = once; return this; } else { throw new Error('Callback must be a function!'); } } removeEventListener(type, callback) { let fns = this.eventCache[type]; if (Array.isArray(fns)) { if (callback === undefined) { fns.length = 0; } else { let idx = fns.indexOf(callback); if (idx !== -1) { fns.splice(idx, 1); } } } return this; } addOnce(type, callback) { this.addEventListener(type, callback, true); } removeAll() { this.eventCache = {}; } dispatch(type, eventData = {}, _this = this) { this.eventCache[type].forEach((fn) => { fn.call(_this, eventData); if (fn.once) { this.removeEventListener(type, fn); } }); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52应用场景
- document.createElement 创建 DOM 元素,便是典型的工厂模式
- 方法内部很复杂,但外部使用很简单。只需要传递标签名,这个方法就会返回对应的 DOM 元素
- document.createElement 创建 DOM 元素,便是典型的工厂模式
优点
- 良好的封装,访问者无需了解创建过程,代码结构清晰
- 扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则
- 解耦了高层逻辑和底层产品类,符合最少知识原则,不需要的就不要去交流
缺点
- 给系统增加了抽象性,带来了额外的系统复杂度,不能滥用(合理抽象能提高系统维护性,但可能会提高阅读难度)
# 适配器模式
实现原理:用于解决兼容问题,接口、方法、数据不兼容,将其转换成访问者期望的格式进行使用
代码实现(获取不同格式的数据)
let data1 = { book_id: 1001 status: 0, create: '2021-12-12 08:10:20', update: '2022-01-15 09:00:00', }; let data2 = { id: 1002 status: 0, createTime: 16782738393022, updateAt: '2022-01-15 09:00:00', }; let data3 = { book_id: 1003 status: 0, createTime: 16782738393022, updateAt: 16782738393022, }; interface bookData { book_id: number; status: number; createAt: string; updateAt: string; } interface bookDataType1 { book_id: number; status: number; create: string; update: string; } interface bookDataType2 { id: number; status: number; createTime: number; updateAt: string; } interface bookDataType3 { book_id: number; status: number; createTime: number; updateAt: number; } const getTimeStamp = function (str: string): number { return timeStamp; }; export const bookDataAdapter = { adapterType1(list: bookDataType1[]) { const bookDataList: bookData[] = list.map((item) => { return { book_id: item.book_id, status: item.status, createAt: getTimeStamp(item.create), updateAt: getTimeStamp(item.update), }; }); return bookDataList; }, adapterType2(list: bookDataType2[]) { const bookDataList: bookData[] = list.map((item) => { return { book_id: item.id, status: item.status, createAt: item.createTime, updateAt: getTimeStamp(item.updateAt), }; }); return bookDataList; }, adapterType3(list: bookDataType3[]) { const bookDataList: bookData[] = list.map((item) => { return { book_id: item.book_id, status: item.status, createAt: item.createTime, updateAt: item.updateAt, }; }); return bookDataList; }, }; const bookDataList = [ ...bookDataAdapter.adapterType1(type1List), ...bookDataAdapter.adapterType2(type2List), ...bookDataAdapter.adapterType3(type3List), ];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96应用场景
- 想要使用一个已经存在的对象,但是接口不满足需求,那么可以使用适配器模式转换成你需要的接口
- 想要创建一个可以复用的对象,而且确定需要和一些不兼容的对象一起工作,这种情况可以使用适配器模式
- axios 既适配了浏览器的 XMLHttpRequest,又适配了 Nodejs 的 http 模块
优点:可以使原有逻辑得到更好的复用,有助于避免大规模改写现有代码
缺点:会让系统变得零乱,明明调用 A,却被适配到了 B,如果滥用,那么对可阅读性不太友好
# 代理模式
实现原理:为一个对象提供一个代用品或占位符,以便控制对它的访问
- 在 ES6 中,使用 Proxy 构建函数能够轻松应用代理模式:
const proxy = new Proxy(target, handler);
- 常见的 js 代理模式
- 缓存代理:为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果
- 虚拟代理:把一些开销很大的对象,延迟到真正需要它的时候才去创建,如图片预加载功能
- 在 ES6 中,使用 Proxy 构建函数能够轻松应用代理模式:
代码实现
// 缓存代理:实现一个求乘积的函数 var multi = function () { var a = 1; for (var i = 0; i < arguments.length; i++) { a = a * arguments[i]; } return a; }; var proxyMult = (function () { var cache = {}; return function () { var args = Array.prototype.join.call(arguments, ','); if (args in cache) { return cache[args]; } else { cache[args] = multi.apply(this, arguments); return cache[args]; } }; })(); proxyMult(1, 2, 3, 4); proxyMult(2, 3, 4, 5); proxyMult(1, 2, 3, 4);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 虚拟代理:实现图片预加载 let myImage = (function () { let imgNode = document.createElement('img'); document.body.appendChild(imgNode); return { setSrc: function (src) { imgNode.src = src; }, }; })(); let proxyImage = (function () { let img = new Image(); img.onload = function () { myImage.setSrc(this.src); }; return { setSrc: function (src) { myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' ); img.src = src; }, }; })(); proxyImage.setSrc('https://xxx.jpg');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27应用场景
- 很多前端框架或者状态管理框架都使用代理模式,用于监听变量的变化
- 使用代理模式代理对象的访问的方式,一般又被称为拦截器,如 axios 的 interceptor 可以提前对服务器返回的数据进行一些预处理
# 策略模式
实现原理:定义一系列算法,根据输入的参数决定使用哪个算法(算法的实现和算法的使用分开)
- Context:封装上下文,按需调用策略,屏蔽外界对策略的直接调用,只对外提供一个接口
- Strategy:含有具体的算法,其方法的外观相同
- StrategyMap:所有策略的合集,供封装上下文调用
代码实现
function priceCalculate(discountType, price) { if (discountType === 'discount200-20') { return price - Math.floor(price / 200) * 20; } else if (discountType === 'discount300-50') { return price - Math.floor(price / 300) * 50; } else if (userType === 'discount500-100') { return price - Math.floor(price / 500) * 100; } } // 1. 随着折扣类型的增加,if-else 会变得越来越臃肿 // 2. 折扣活动算法改变或折扣类型增加时,都需要改动 priceCalculate 方法,违反开闭原则 // 3. 复用性差,如果其他地方有类似的算法,但规则不一样,上述代码不能复用
1
2
3
4
5
6
7
8
9
10
11
12
13const discountMap = { 'discount200-20': function (price) { return price - Math.floor(price / 200) * 20; }, 'discount300-50': function (price) { return price - Math.floor(price / 300) * 50; }, 'discount500-100': function (price) { return price - Math.floor(price / 500) * 100; }, }; function priceCalculate(discountType, price) { return discountMap[discountType] && discountMap[discountType](price); } // 将算法的实现和算法的使用分开,但抽象程度并不高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const priceCalculate = (function () { const discountMap = { 'discount200-20': function (price) { return price - Math.floor(price / 200) * 20; }, 'discount300-50': function (price) { return price - Math.floor(price / 300) * 50; }, 'discount500-100': function (price) { return price - Math.floor(price / 500) * 100; }, }; return { addStategy(stategyName, fn) { if (discountMap[stategyName]) return; discountMap[stategyName] = fn; }, priceCal(discountType, price) { return discountMap[discountType] && discountMap[discountType](price); }, }; })(); priceCalculate.priceCal('discount200-20', 250); priceCalculate.addStategy('discount800-200', function (price) { return price - Math.floor(price / 800) * 200; }); // 隐藏算法的实现:借助 IIFE 使用闭包的方式,提供一个添加策略的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29应用场景
- 多个算法只有行为上有些不同,可以考虑策略模式动态选择算法;需要多重判断,可以考虑策略模式规避多重条件判断。
- 使用 Redux 时 action 的分派。
优点:策略相互独立,可以互相切换,提高了灵活性、复用性以及可维护性;可扩展性好,满足开闭原则。
缺点
- 策略相互独立,一些复杂的算法逻辑无法共享,造成资源浪费。
- 用户在使用策略时,需要了解具体的策略实现(不满足最少知识原则,增加了使用成本)。
# 观察者模式
实现原理:将多个 observer(观察者) 注册到某个对象(被观察者)上,当该对象发生变更时,便自动通知其上的所有 observer 进行更新。
代码实现
let observerIds = 0; class Subject { constructor() { this.observers = []; } addObserver(observer) { this.observers.push(observer); } removeObserver(observer) { this.observers = this.observers.filter((obs) => { return obs.id !== observer.id; }); } notify() { this.observers.forEach((observer) => observer.update(this)); } } class Observer { constructor() { this.id = observerIds++; } update(subject) { // Todo: update something } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31应用场景
- Vue 双向绑定中的发布订阅模式
- 简述:响应式数据相当于被观察者;组件相当于观察者;响应式数据发生变更会通知相关组件进行视图更新。
- Vue 通过 defineProperty 或者 proxy 劫持各数据的 setter 和 getter,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据的组件,响应式后的数据相当于消息的发布者。
- 每个组件都对应一个 Watcher 订阅者,当组件渲染函数执行时,会将本组件的 Watcher 加入到所依赖的响应式数据的订阅者列表中,相当于完成了一次订阅,这个过程叫做依赖收集。
- 当响应式数据发生变化时,会触发 setter,setter 负责通知数据的订阅者列表中的 Watcher,Watcher 触发组件重新渲染来更新视图。
- EventEmitter
- Vue 双向绑定中的发布订阅模式
优点:目标变化就会通知观察者
- 时间解耦:注册的订阅行为由发布者决定何时调用,订阅者无需持续关注,由发布者负责通知
- 对象解耦:发布者无需知道消息的接受者,只需遍历订阅该消息类型的订阅者发送消息,解耦了发布者和订阅者之间的联系
缺点:目标和观察者耦合度高,必须同时引入被观察者和观察者才能达到响应式的效果
- 资源消耗:创建订阅者需要一定的时间和内存
- 增加复杂度:弱化了联系,难以维护调用关系,增加了理解成本