我们写的代码都是为了一定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展性不好,可能所有代码都需要重写,那就是一场灾难了,所以提高代码的扩展性是势在必行的。怎样才算有好的扩展性呢?好的扩展性应该具备以下特征:
需求变更时,代码不需要重写。局部代码的修改不会引起大规模的改动。有时候我们去重构一小块代码,但是发现他跟其他代码都是杂糅在一起的,里面各种耦合,一件事情拆在几个地方做,要想改这一小块必须要改很多其他代码。那说明这些代码的耦合太高,扩展性不强。可以很方便的引入新功能和新模块。
当然是从优秀的代码身上学习了,本文会深入Axios
,Node.js
,Vue
等优秀框架,从他们源码总结几种设计模式出来,然后再用这些设计模式尝试解决下工作中遇到的问题。本文主要会讲职责链模式
,观察者模式
,适配器模式
,装饰器模式
。下面一起来看下吧:
职责链模式顾名思义就是一个链条,这个链条上串联了很多的职责,一个事件过来,可以被链条上的职责依次处理。他的好处是链条上的各个职责,只需要关心自己的事情就行了,不需要知道自己的上一步是什么,下一步是什么,跟上下的职责都不耦合,这样当上下职责变化了,自己也不受影响,往链条上添加或者减少职责也非常方便。
用过 Axios 的朋友应该知道,Axios 的拦截器有请求拦截器
和响应拦截器
,执行的顺序是请求拦截器 -> 发起请求 -> 响应拦截器
,这其实就是一个链条上串起了三个职责。下面我们来看看这个链条怎么实现:
// 先从用法入手,一般我们添加拦截器是这样写的
// instance.interceptors.request.use(fulfilled, rejected)
// 根据这个用法我们先写一个`Axios`类。
function Axios() {
// 实例上有个interceptors对象,里面有request和response两个属性
// 这两个属性都是InterceptorManager的实例
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
// 然后是实现InterceptorManager类
function InterceptorManager() {
// 实例上有一个数组,存储拦截器方法
this.handlers = [];
}
// InterceptorManager有一个实例方法use
InterceptorManager.prototype.use = function(fulfilled, rejected) {
// 这个方法很简单,把传入的回调放到handlers里面就行
this.handlers.push({
fulfilled,
rejected
})
}
上面的代码其实就完成了拦截器创建和use
的逻辑,并不复杂,那这些拦截器方法都是什么时候执行呢?当然是我们调用instance.request
的时候,调用instance.request
的时候真正执行的就是请求拦截器 -> 发起请求 -> 响应拦截器
链条,所以我们还需要来实现下Axios.prototype.request
:
Axios.prototype.request = function(config) {
// chain里面存的就是我们要执行的方法链条
// dispatchRequest是发起网络请求的方法,本文主要讲设计模式,这个方法就不实现了
// chain里面先把发起网络请求的方法放进去,他的位置应该在chain的中间
const chain = [dispatchRequest, undefined];
// chain前面是请求拦截器的方法,从request.handlers里面取出来放进去
this.interceptors.request.handlers.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// chain后面是响应拦截器的方法,从response.handlers里面取出来放进去
this.interceptors.response.handlers.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 经过上述代码的组织,chain这时候是这样的:
// [request.fulfilled, request.rejected, dispatchRequest, undefined, response.fulfilled,
// response.rejected]
// 这其实已经按照请求拦截器 -> 发起请求 -> 响应拦截器的顺序排好了,拿来执行就行
let promise = Promise.resolve(config); // 先来个空的promise,好开启then
while (chain.length) {
// 用promise.then进行链式调用
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
上述代码是从 Axios 源码中精简出来的,可以看出他巧妙的运用了职责链模式,将需要做的任务组织成一个链条,这个链条上的任务相互不影响,拦截器可有可无,而且可以有多个,兼容性非常强。
看了优秀框架对职责链模式的运用,我们再看看在我们平时工作中这个模式怎么运用起来。现在假设有这样一个需求是做一个表单验证,这个验证需要前端先对格式等内容进行校验,然后 API 发给后端进行合法性校验。我们先分析下这个需求,前端校验是同步的,后端验证是异步的,整个流程是同步异步交织的,为了能兼容这种情况,我们的每个验证方法的返回值都需要包装成 promise 才行
// 前端验证先写个方法
function frontEndValidator(inputValue) {
return Promise.resolve(inputValue); // 注意返回值是个promise
}
// 后端验证也写个方法
function backEndValidator(inputValue) {
return Promise.resolve(inputValue);
}
// 写一个验证器
function validator(inputValue) {
// 仿照Axios,将各个步骤放入一个数组
const validators = [frontEndValidator, backEndValidator];
// 前面Axios是循环调用promise.then来执行的职责链,我们这里换个方式,用async来执行下
async function runValidate() {
let result = inputValue;
while(validators.length) {
result = await validators.shift()(result);
}
return result;
}
// 执行runValidate,注意返回值也是一个promise
runValidate().then((res) => {console.log(res)});
}
// 上述代码已经可以执行了,只是我们没有具体的校验逻辑,输入值会原封不动的返回
validator(123); // 输出: 123
上述代码我们用职责链模式组织了多个校验逻辑,这几个校验之间相互之间没有依赖,如果以后需要减少某个校验,只需要将它从validators
数组中删除即可,如果要添加就往这个数组添加就行了。这几个校验器之间的耦合度就大大降低了,而且他们封装的是 promise,完全还可以用到其他模块去,其他模块根据需要组织自己的职责链就行了。
观察者模式还有个名字叫发布订阅模式,这在 JS 的世界里可是大名鼎鼎,大家或多或少都用到过,最常见的就是事件绑定了,有些面试还会要求面试者手写一个事件中心,其实就是一个观察者模式。观察者模式的优点是可以让事件的产生者和消费者相互不知道,只需要产生和消费相应的事件就行,特别适合事件的生产者和消费者不方便直接调用的情况,比如异步中。我们来手写一个看看:
class PubSub {
constructor() {
// 一个对象存放所有的消息订阅
// 每个消息对应一个数组,数组结构如下
// {
// "event1": [cb1, cb2]
// }
this.events = {}
}
subscribe(event, callback) {
if(this.events[event]) {
// 如果有人订阅过了,这个键已经存在,就往里面加就好了
this.events[event].push(callback);
} else {
// 没人订阅过,就建一个数组,回调放进去
this.events[event] = [callback]
}
}
publish(event, ...args) {
// 取出所有订阅者的回调执行
const subscribedEvents = this.events[event];
if(subscribedEvents && subscribedEvents.length) {
subscribedEvents.forEach(callback => {
callback.call(this, ...args);
});
}
}
unsubscribe(event, callback) {
// 删除某个订阅,保留其他订阅
const subscribedEvents = this.events[event];
if(subscribedEvents && subscribedEvents.length) {
this.events[event] = this.events[event].filter(cb => cb !== callback)
}
}
}
// 使用的时候
const pubSub = new PubSub();
pubSub.subscribe('event1', () => {}); // 注册事件
pubSub.publish('event1'); // 发布事件
观察者模式的一个典型应用就是 Node.js 的 EventEmitter,我有另一篇文章从发布订阅模式入手读懂 Node.js 的 EventEmitter 源码从异步应用的角度详细讲解了观察者模式的原理和 Node.js 的 EventEmitter 源码,我这里就不重复书写了,上面的手写代码也是来自这篇文章。
一样的,看了优秀框架的源码,我们自己也要试着来用一下,这里的例子是转圈抽奖。想必很多朋友都在网上抽过奖,一个转盘,里面各种奖品,点一下抽奖,然后指针开始旋转,最后会停留到一个奖品那里。我们这个例子就是要实现这样一个 Demo,但是还有一个要求是每转一圈速度就加快一点。我们来分析下这个需求: