《JavaScript设计模式与开发实践》读书笔记

《JavaScript设计模式与开发实践》读书笔记

突然意识到设计模式的重要性,于是找了《JavaScript设计模式与开发实践》PDF来看。总的来说,不是很喜欢。示例都有点老旧,观点不一定正确(没有区分观察者模式和发布订阅模式)。但我可以用其结构,结合现在、系统的梳理一遍前端里用到的设计模式。

很好的网站

单例模式

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

最常见的例子就是toast。之前封装Vue的toast和modal组件时就是个典型例子。toast应该始终只有一个,如果toast多次,始终只toast一个内容。只是替换了文字和重置关闭时间。因为用户不太希望toast n多层内容,导致文字重叠。

model就不是单例。可以叠多层上去。

策略模式

策略模式的定义是:定义一系列的算法,把他们一个个封装起来,并且使他们可以互相替换。

个人理解,策略模式就类似React根据tag类型去做对应操作的方式。如beginWork的mount阶段,根据tag的类型创建不同的fiber。layout阶段commitLayoutEffectOnFiber会根据tag去执行不同的commit操作。

个人开发中也有,全员的海报需求时,合并公文包与内容集的分享弹窗合并小程序码和二维码海报时,重构前是四个接口,合并后是一个接口。四个接口参数都不相同。其实可以先制定好四种策略,在实际调用中连续调用四个策略即可。

但网上说的,微微有点不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 客户端代码会选择具体策略并将其传递给上下文。客户端必须知晓策略之间的差
// 异,才能做出正确的选择。
class ExampleApplication is
method main() is

创建上下文对象。

读取第一个数。
读取最后一个数。
从用户输入中读取期望进行的行为。

if (action == addition) then
context.setStrategy(new ConcreteStrategyAdd())

if (action == subtraction) then
context.setStrategy(new ConcreteStrategySubtract())

if (action == multiplication) then
context.setStrategy(new ConcreteStrategyMultiply())

result = context.executeStrategy(First number, Second number)

不同的策略通过setStrategy注入context,然后执行executeStrategy即可。JS里,setStrategy的参数,可以是个方法,而不是new对象
况且,与其先set一遍strategy再excuteStrategy,还不如直接在条件内excute对应的strategy。所以我理解为,我理解的与网上的是一致的。

代理模式

代理模式就太多了。

ES6的proxydebouncethrottle。对真正执行操作的方法前加一层代理,可以做到,真正剥离纯逻辑与访问控制。是一个很良好的思想。

其实实际开发中,应该常用代理模式。

如很多方法其实系统已经提供,但业务中不应该直接用。如this.$router.replace、Taro.navigateTo、document.cookie、axios()等。最好业务中应该使用封装过后在调用。一旦之后需要加什么功能,就可以在代理中加,而不是全局替换一遍方法。

遇到一个坑是,工作台的路由跳转一直用的是react-router-dom原生的跳转方法。但是后来需要兼容腾讯云,不得不封装了一个setLink方法,然后手动的一个一个替换过来。

发布订阅与观察者

这™应该是最争议的模式了。到底发布订阅模式与观察者模式是不是一种模式?本书与Refactoring.Guru都认为是一种。但是民间都认为是两种。

不过我个人也认为,是两种。

观察者模式

观察者模式是一对多的关系,最常见的方法是MutationObserverResizeObserver

1
2
3
4
5
const observer = new MutationObserver(callback);

// 以上述配置开始观察目标节点
observer.observe(targetNode);

observer对象作为观察者,观察targetNode,当targetNode有改变时,做出响应。

发布订阅模式

发布订阅之余观察者多了一个事件中心的概念。是一个事件对多个订阅者、多个发布者。

发布订阅,最常见的是Vue的eventBus、DOM的事件了(很多事件的概念都如此)。

总结

发布订阅的三个角色能更好的解耦,其实更多情况,发布订阅优于观察者模式。

工厂模式

工厂模式说简单也简单,说复杂也很复杂。

简单说,工厂模式就如其名,给一个工厂函数传入指定类型参数,即可返回期望类型的内容。

工厂模式行为类似类,但不仅限于类。类更多的是创建一个对象,或者根据参数创建一个对象。

工厂模式更多是,根据类型直接生成一个期望类型的内容。

简单的实现方法

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
//User类
class User {
//构造器
constructor(opt) {
this.name = opt.name;
this.viewPage = opt.viewPage;
}

//静态方法
static getInstance(role) {
switch (role) {
case 'superAdmin':
return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
break;
case 'admin':
return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
break;
case 'user':
return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
break;
default:
throw new Error('参数错误, 可选参数:superAdmin、admin、user')
}
}
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');

如果说复杂的,工厂模式还分为简单工厂模式工厂方法模式抽象工厂模式。后两者还有抽象类的概念,在JS的摇篮里待久了,不太能理解。。

所幸,前端一般就用到简单工厂模式。暂时不考虑另外两种。

实际场景

本段末尾贴的链接给得挺好。例子提到的Vue-router根据用户权限动态配置路由。

那么就可以通过传入用户类型,生产出路由列表,然后动态创建路由即可

这篇文章说得挺好

命令模式

个人理解,命令模式就是希望把逻辑解耦,然后逻辑通过命令调用。

那么这个模式应该早已深入骨髓。

实际场景

在编写React组件时,往往要绑定事件回调,那么可以这么写:

1
2
3
function Copy(props){
return <button onClick={() => console.log('do Copy' + props.text)}>button</button>
}

而更好的做法是(命令模式)把逻辑与UI分离

1
2
3
4
5
6
function Copy(props){
const handleCopy = () => {
console.log('do Copy' + props.text)
}
return <button onClick={handleCopy}>button</button>
}

copy可能多个地方会用,如此解耦,就不用多次编写逻辑代码了。

外观模式

外观模式(Facade Pattern)隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

个人理解,各种库、sdk的的设计就是外观模式。比如说微信jssdk,没人知道如何与微信浏览器打交道,使用着只管调用sdk提供的api就能完成一系列的功能

组合模式

按照我个人理解,组合模式就是把一系列操作组合成一个树。要进行操作时,只需对数的根节点触发,组合模式则应该遍历树,对每个叶子节点执行操作方法。

就这么简单。。。

模板方法模式

书上描述的模板方法模式是:模板方法模式建议将算法分解为一系列步骤, 然后将这些步骤改写为方法, 最后在 “模板方法” 中依次调用这些方法

《JavaScript设计模式与开发实践》的例子是,煮咖啡与泡茶。

煮咖啡与泡茶都有类似的步骤:煮水、冲泡、倒进杯子、加料。

尽管咖啡与茶有不同的操作,如,加料部分、咖啡是糖和牛奶,茶是柠檬。但是步骤都是一样的。

所以可以把冲泡每一步骤分解成不同的算法。然后再执行方法里,组合这些算法

1
2
3
4
5
6
7
8
9
10
11
12
13

var Beverage = function() {};

Beverage.prototype.boilWater = function(){};
Beverage.prototype.brew = function(){};
Beverage.prototype.pourInCup = function(){};
Beverage.prototype.addCondiments = function(){};
Beverage.prototype.init = function() {
this.boilWarter();
this.brew();
this.pourInCur();
this.addCondiments();
}

init方法就是模板方法模式。由子类去实现不同的算法,init拼接模板执行。

书中还说道,Vue、React的生命周期的钩子,也是模板方法模式。

享元模式

享元模式英文名是flyweight,fly是苍蝇、意思是蝇量级,也就是轻量级的意思(不要去记中文名!!!)

refactoringguru网站的定义是:它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象

中间慢慢推论就不说了,实际应用场景是:XX池

实际场景

比如做一个滚动的日历组件,每一日的小块都是一个对象(或者说组件),实际情况中,页面同时应该只会存在两个月,也就是62个日组件,这样,就可以在加载下一个月的时候,复用被滚出的日组件。内存中始终存一个“日组件池”,这样防止无限滚动时,内存中无限存储日组件。

这其实是个时间换空间的模式。这样一来,加载下一个月时,需要牺牲一定的性能来换取空间。一般是日历这种可能会无限加载的内容会用到享元模式。

职责链模式

refactoringguru的解释是:允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者

其实也很好理解,JavaScript中的作用域链、原型链、DOM的事件冒泡就是最经典的职责链模式。

实际场景

手写表单验证时,一般我们会对一个输入进行多重验证。每一重就是链上的一个节点。如果节点能处理,则return false,如果校验成功,则交给链的下一个节点

中介者模式

书与网站都提到了一个例子:机场的塔台与飞机。

机场飞机起落需要所有飞机协同好时间,一次只能起降一辆飞机,否则事故会非常的严重。

如果没有中介者,则需要飞机与附近的所有飞机都进行沟通、协商。而实际上,所有飞机都只与塔台联系,省去了与多个飞机沟通的难题。

塔台则是中介者。

中介者模式使用场景是,如果遇到多个对象,多个对象之间互相都有一些联系。如果某些情况需要改动代码,则牵一发而动全身,要改所有有关联的对象。这就会带来隐患。

多个对象网状的关系这种情况,可以引入一个中介。所有对象都通过中介来沟通,所有需要与其他对象的操作,与所有其他对象对当前对象的造成的改变的方法都放到中介那。中介协调各个对象之间的关系。

现实中,全局store,如Redux、mobx这些应该就是中介的角色

中介模式也会带来一些问题,就是,需要引入一个额外中介对象。而且随着中介逻辑的增多,中介对象也会变得难以维护。

装饰者模式

这个也是深入骨髓的模式了,ES6的装饰器就是最经典的例子

1
2
3
4
5
6
7
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}

在ES6出来之前也有装饰器模式:

1
2
3
4
5
6
7
8
9
10
window.onload = function() {
alert(1)
}

var _onload = window.onload || function(){};

window.onload = function(){
_onload();
alert(2);
}

状态模式

状态模式的定义:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类

定义有点晦涩难懂。书中用个例子来解释,

假设我们有个台灯,开关可以控制台灯不同亮度:off -> 1 -> 2 ->3 -> 4 -> off

一般情况下,我们需要写一堆if else来判断,什么状态下,需要跳转到什么状态,并且要做什么操作。

利用状态模式改造,需要把每个状态都抽象为一个类,状态类定义点击事件,决定下一步是什么状态。

emmm

这么说,有点像策略模式?确实有点像。

状态模式还可以深入一点,需要理解有限状态机的概念。

有限状态机,大概意思就是,维护一个二维表,分别表示状态之间切换到条件。在状态切换时,从表里找到对应的操作。这样非常清晰明了。(详细了解看阮一峰的文章

实际场景

一般用在状态比较多,需要写一堆if else的情况,如ajax的状态,文件上传的状态等。暂时没有在生产中用到。

适配器模式

这个也是深入骨髓的模式了。一般用在兼容旧接口的情况下用到。

原则

单一职责原则(SRP)

就一个类而言,应该有一个引起它变化的原因。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。每个职责都是变化的一个轴线,如果一个方法承担太多职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大,产生bug的可能性就越大。

实际中,方法、类、以及React hooks的依赖,都应该这样设计,否则方法太大,非常难改

反例:内容营销小程序

最少知识原则(LKP)

最少知识原则说的是一个软件实体应当尽可能少的与其他实体发生相互作用。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。

其实也与SRP目的类似,就是软件实体尽量减少与其他实体的关联,也就减少了复杂性、减少需求变更时被修改的概率。从而减少bug的产生。

实际中,方法、模块的引用都应该尽量减少网状的引用。保持树状的引用关系,修改起来比较方便。

反例:全员与获客通相关的页面。全都柔和在一起,分包时没法拆开。

开放-封闭原则

其定义是:软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改。

书中举例:扩展onload

假设onload已经绑定了一个巨型函数,要添加功能的时候,修改这个巨型函数肯定是非常不理智的。正确的做法是,用装饰者模式,灵活的给其添加功能

我个人理解是,一个良好的设计就是:shell命令那种。linux由一堆灵活的命令组成,那么扩展起来,就非常的方便。开发者可以随意组合这些命令以实现强大的功能。

反之,windows,根本没法扩展。。。

分类

创建型模式

创建型模式提供了创建对象的机制, 能够提升已有代码的灵活性和可复用性。

如:单例模式、工厂模式

结构型模式

结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效。

如:适配器模式、组合模式、装饰器模式、代理模式、享元模式等。

行为模式

行为模式负责对象间的高效沟通和职责委派。

如:状态模式、中介者模式、责任链模式、观察则模式、策略模式等。