发布订阅模式
订阅-发布模式定义了对象之间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都可以得到通知。
了解过事件机制或者函数式编程的朋友,应该会体会到“订阅-发布模式”所带来的“时间解耦”和“空间解耦”的优点。
借助函数式编程中闭包和回调的概念,可以很优雅地实现这种设计模式。
“订阅-发布模式” vs 观察者模式
订阅-发布模式和观察者模式概念相似,但在订阅-发布模式中,订阅者和发布者之间多了一层中间件:一个被抽象出来的信息调度中心。
最大的特点是集中管理,统一触发。
JS中一般用事件模型来代替传统的发布-订阅模式。任何一个对象的原型链被指向Event
的时候,这个对象便可以绑定自定义事件和对应的回调函数。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
79const Event = {
clientList: {},
// 绑定事件监听
listen(key, fn){
if(! this.clientList[key]){
this.clientList[key] = [];
}
this.clientList[key].push(fn);
return true;
},
// 触发对应事件
trigger(){
const key = Array.prototype.shift.apply(arguments),
fns = this.clientList[key];
if(!fns || fns.length === 0){
return false;
}
for(let fn of fns){
fn.apply(null, arguments);
}
return true;
},
// 移除相关事件
remove(key, fn){
let fns = this.clientList[key];
// 如果之前没有绑定事件
// 或者没有指明要移除的事件
// 直接返回
if(!fns || !fn){
return false;
}
// 反向遍历移除置指定事件函数
for(let l = fns.length - 1; l >= 0; l--){
let _fn = fns[l];
if(_fn === fn){
fns.splice(l, 1);
}
}
return true;
}
}
// 为对象动态安装 发布-订阅 功能
const installEvent = (obj) => {
for(let key in Event){
obj[key] = Event[key];
}
}
let salesOffices = {};
installEvent(salesOffices);
// 绑定自定义事件和回调函数
salesOffices.listen("event01", fn1 = (price) => {
console.log("Price is", price, "at event01");
})
salesOffices.listen("event02", fn2 = (price) => {
console.log("Price is", price, "at event02");
})
salesOffices.trigger("event01", 1000);
salesOffices.trigger("event02", 2000);
salesOffices.remove("event01", fn1);
// 输出: false
// 说明删除成功
console.log(salesOffices.trigger("event01", 1000));
状态模式
状态模式:对象行为是基于状态来改变的。
内部的状态转化,导致了行为表现形式不同。
所以,用户在外面看起来,好像是修改了行为。
优缺点
优点
封装了转化规则,对于大量分支语句,可以考虑使用状态类进一步封装。
每个状态都是确定的,所以对象行为是可控的。
缺点
状态模式的关键是将事物的状态都封装成单独的类,这个类的各种方法就是“此种状态对应的表现行为”。
因此,状态类会增加程序开销。
在JavaScript中,可以直接用JSON对象来代替状态类。
下面代码展示的就是FSM(有限状态机)里面有3种状态:download、pause、deleted。
控制状态转化的代码也在其中。
DownLoad类就是,常说的Context对象,它的行为会随着状态的改变而改变。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
58const FSM = (() => {
let currenState = "download";
return {
download: {
click: () => {
console.log("暂停下载");
currenState = "pause";
},
del: () => {
console.log("先暂停, 再删除");
}
},
pause: {
click: () => {
console.log("继续下载");
currenState = "download";
},
del: () => {
console.log("删除任务");
currenState = "deleted";
}
},
deleted: {
click: () => {
console.log("任务已删除, 请重新开始");
},
del: () => {
console.log("任务已删除");
}
},
getState: () => currenState
};
})();
class Download {
constructor(fsm) {
this.fsm = fsm;
}
handleClick() {
const { fsm } = this;
fsm[fsm.getState()].click();
}
hanldeDel() {
const { fsm } = this;
fsm[fsm.getState()].del();
}
}
// 开始下载
let download = new Download(FSM);
download.handleClick(); // 暂停下载
download.handleClick(); // 继续下载
download.hanldeDel(); // 下载中,无法执行删除操作
download.handleClick(); // 暂停下载
download.hanldeDel(); // 删除任务
装饰者模式
装饰者模式:在不改变对象自身的基础上,动态地添加功能代码。
根据描述,装饰者显然比继承等方式更灵活,而且不污染原来的代码,代码逻辑松耦合。
应用场景
装饰者模式由于松耦合,多用于一开始不确定对象的功能、或者对象功能经常变动的时候。
尤其是在参数检查、参数拦截等场景。
ES6的装饰器语法规范只是在“提案阶段”,而且不能装饰普通函数或者箭头函数。
下面的代码,addDecorator可以为指定函数增加装饰器。
其中,装饰器的触发可以在函数运行之前,也可以在函数运行之后。
注意:装饰器需要保存函数的运行结果,并且返回。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
38const addDecorator = (fn, before, after) => {
let isFn = fn => typeof fn === "function";
if (!isFn(fn)) {
return () => {};
}
return (...args) => {
let result;
// 按照顺序执行“装饰函数”
isFn(before) && before(...args);
// 保存返回函数结果
isFn(fn) && (result = fn(...args));
isFn(after) && after(...args);
// 最后返回结果
return result;
};
};
/******************以下是测试代码******************/
const beforeHello = (...args) => {
console.log(`Before Hello, args are ${args}`);
};
const hello = (name = "user") => {
console.log(`Hello, ${name}`);
return name;
};
const afterHello = (...args) => {
console.log(`After Hello, args are ${args}`);
};
const wrappedHello = addDecorator(hello, beforeHello, afterHello);
let result = wrappedHello("godbmw.com"); //高阶函数
console.log(result);
责任链模式
责任链模式:多个对象均有机会处理请求,从而解除发送者和接受者之间的耦合关系。这些对象连接成为链式结构,每个节点转发请求,直到有对象处理请求为止。
其核心就是:请求者不必知道是谁哪个节点对象处理的请求。如果当前不符合终止条件,那么把请求转发给下一个节点处理。
而当需求具有“传递”的性质时(代码中其中一种体现就是:多个if、else if、else if、else嵌套),就可以考虑将每个分支拆分成一个节点对象,拼接成为责任链。
优点与代价
优点
可以根据需求变动,任意向责任链中添加 / 删除节点对象
没有固定的“开始节点”,可以从任意节点开始
代价
责任链最大的代价就是每个节点带来的多余消耗。当责任链过长,很多节点只有传递的作用,而不是真正地处理逻辑。
代码实现
为了方便演示,模拟常见的“日志打印”场景。模拟了 3 种级别的日志输出:1
2
3LogHandler: 普通日志
WarnHandler:警告日志
ErrorHandler:错误日志
首先我们会构造“责任链”:LogHandler -> WarnHandler -> ErrorHandler。LogHandler作为链的开始节点。
如果是普通日志,那么就由 LogHandler 处理,停止传播;如果是 Warn 级别的日志,那么 LogHandler 就会自动向下传递,WarnHandler 接收到并且处理,停止传播;Error 级别日志同理。
1 | class Handler { |
享元模式
享元模式:运用共享技术来减少创建对象的数量,从而减少内存占用、提高性能。
享元模式提醒我们将一个对象的属性划分为内部和外部状态。
内部状态:可以被对象集合共享,通常不会改变
外部状态:根据应用场景经常改变
享元模式是利用时间换取空间的优化模式。
应用场景
享元模式虽然名字听起来比较高深,但是实际使用非常容易:只要是需要大量创建重复的类的代码块,均可以使用享元模式抽离内部/外部状态,减少重复类的创建。
为了显示它的强大,下面的代码是简单地实现了大家耳熟能详的“对象池”,以彰显这种设计模式的魅力。
代码实现
这里利用javascript实现了一个“通用对象池”类–ObjectPool。这个类管理一个装载空闲对象的数组,如果外部需要一个对象,直接从对象池中获取,而不是通过new操作。
对象池可以大量减少重复创建相同的对象,从而节省了系统内存,提高运行效率。
为了形象说明“享元模式”在“对象池”实现和应用,特别准备了模拟了File类,并且模拟了“文件下载”操作。
通过阅读下方代码可以发现:对于File类,内部状态是pool属性和download方法;外部状态是name和src(文件名和文件链接)。借助对象池,实现了File类的复用。
注:为了方便演示,Javascript实现的是并发操作。输出结果略有不同。
1 | // 对象池 |
输出结果如下:1
2
3
4
5
6
7
8+ 从 https://download1.com 开始下载 文件1
+ 从 https://download2.com 开始下载 文件2
- 文件1 下载完毕
- 文件2 下载完毕
+ 从 https://download3.com 开始下载 文件3
- 文件3 下载完毕
**************************************************
下载了3个文件,但其实只创建了2个对象
组合模式
组合模式,将对象组合成树形结构以表示“部分-整体”的层次结构。
用小的子对象构造更大的父对象,而这些子对象也由更小的子对象构成
单个对象和组合对象对于用户暴露的接口具有一致性,而同种接口不同表现形式亦体现了多态性
应用场景
组合模式可以在需要针对“树形结构”进行操作的应用中使用,例如扫描文件夹、渲染网站导航结构等等。
代码实现
这里用代码模拟文件扫描功能,封装了File和Folder两个类。在组合模式下,用户可以向Folder类嵌套File或者Folder来模拟真实的“文件目录”的树结构。
同时,两个类都对外提供了scan接口,File下的scan是扫描文件,Folder下的scan是调用子文件夹和子文件的scan方法。整个过程采用的是深度优先。
1 | // 文件类 |
执行$ node main.js
,最终输出结果是:1
2
3
4
5
6扫描文件夹: 用户根目录
扫描文件夹: 第一个文件夹
扫描文件: 1号文件
扫描文件夹: 第二个文件夹
扫描文件: 2号文件
扫描文件: 3号文件
命令模式
命令模式是一种数据驱动的设计模式,它属于行为型模式。
请求以命令的形式包裹在对象中,并传给调用对象。
调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象。
该对象执行命令。
在这三步骤中,分别有3个不同的主体:发送者、传递者和执行者。在实现过程中,需要特别关注。
应用场景
有时候需要向某些对象发送请求,但是又不知道请求的接受者是谁,更不知道被请求的操作是什么。此时,命令模式就是以一种松耦合的方式来设计程序。
setCommand方法为按钮指定了命令对象,命令对象为调用者(按钮)找到了接收者(MenuBar),并且执行了相关操作。而按钮本身并不需要关心接收者和接受操作。
1 | // 接受到命令,执行相关操作 |
下面是同级目录的html代码,在谷歌浏览器中打开创建的index.html,并且打开控制台,即可看到效果。1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>命令模式</title>
</head>
<body>
<button>按钮</button>
<script src="./main.js"></script>
</body>
</html>
迭代器模式
迭代器模式是指提供一种方法顺序访问一个集合对象的各个元素,使用者不需要了解集合对象的底层实现。
内部迭代器和外部迭代器
内部迭代器:封装的方法完全接手迭代过程,外部只需要一次调用。
外部迭代器:用户必须显式地请求迭代下一元素。熟悉C++的朋友,可以类比C++内置对象的迭代器的 end()、next()等方法。
这里实现的是一个外部迭代器。需要实现边界判断函数、元素获取函数和更新索引函数。
1 | const Iterator = obj => { |
代理模式
代理模式的定义:为一个对象提供一种代理以方便对它的访问。
代理模式可以解决避免对一些对象的直接访问,以此为基础,常见的有保护代理和虚拟代理。保护代理可以在代理中直接拒绝对对象的访问;虚拟代理可以延迟访问到真正需要的时候,以节省程序开销。
代理模式优缺点
代理模式有高度解耦、对象保护、易修改等优点。
同样地,因为是通过“代理”访问对象,因此开销会更大,时间也会更慢。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// main.js
const myImg = {
setSrc(imgNode, src) {
imgNode.src = src;
}
};
// 利用代理模式实现图片懒加载
const proxyImg = {
setSrc(imgNode, src) {
myImg.setSrc(imgNode, "./image.png"); // NO1. 加载占位图片并且将图片放入<img>元素
let img = new Image();
img.onload = () => {
myImg.setSrc(imgNode, src); // NO3. 完成加载后, 更新 <img> 元素中的图片
};
img.src = src; // NO2. 加载真正需要的图片
}
};
let imgNode = document.createElement("img"),
imgSrc =
"https://upload-images.jianshu.io/upload_images/5486602-5cab95ba00b272bd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp";
document.body.appendChild(imgNode);
proxyImg.setSrc(imgNode, imgSrc);
1 | <!-- main.html --> |
策略模式
策略模式定义:就是能够把一系列“可互换的”算法封装起来,并根据用户需求来选择其中一种。
策略模式实现的核心就是:将算法的使用和算法的实现分离。算法的实现交给策略类。算法的使用交给环境类,环境类会根据不同的情况选择合适的算法。
策略模式优缺点
在使用策略模式的时候,需要了解所有的“策略”(strategy)之间的异同点,才能选择合适的“策略”进行调用。
1 | // 策略类 |
单例模式
单例模式定义:保证一个类仅有一个实例,并提供访问此实例的全局访问点。
单例模式用途
如果一个类负责连接数据库的线程池、日志记录逻辑等等,此时需要单例模式来保证对象不被重复创建,以达到降低开销的目的。
1 | const Singleton = function() {}; |