前言
设计模式系列:
前端设计模式之工厂模式
前端设计模式之代理模式
前端设计模式之策略模式
前端设计模式之装饰模式
责任链模式
什么是责任链模式
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(此处引自 gof 设计模式)
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,所以责任链将请求的发送者和请求的处理者解耦了。
责任链模式是一种对象行为型模式,其主要优点如下:
- 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
- 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
其主要缺点如下。
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
其他说明
责任链模式,总的一个核心就是请求者不必知道是谁哪个节点对象处理的请求,由于处理请求的可以在不同对象下处理,所以请求者跟接受者是解耦的。
纯的责任链:要求请求在这些对象链中必须被处理,而且一个节点处理对象,要么只处理请求,要么把请求转发给下个节点对象处理;
不纯的责任链:要求在责任链里不一定会有处理结构,而且一个节点对象,即可以处理部分请求,并把请求再转发下个节点处理;
javascript 中介者模式
责任链模式对前端开发来说可能有点陌生,但是看了前面的描述又感觉似曾相识
实际上 express、redux 里的 middleware 都可以简单理解为责任链模式的运用
要实现中间件模式,最重要的实现细节是:
- 可以通过调用 use() 函数来注册新的中间件
- 当接收到需要处理的新数据时,注册的中间件在执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值
- 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的回调函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件
项目实战
通用中间件开发
class Middleware {
constructor() {
this.$cache = []
this.$middlewares = []
}
// 注册中间件
use() {
[...arguments].forEach(item => {
if (typeof item === 'function') {
this.$cache.push(item)
}
})
return this
}
/**
* 每个中间件只有两个形参 第一是传进来的参数 第二个是调用下一个中间件的函数
* 中间件的执行顺序是根据你注册中间件的顺序来去调用的
*/
next(params) {
while (this.$middlewares.length) {
const ware = this.$middlewares.shift()
ware.call(this, params, this.next.bind(this))
}
}
execute(params) {
this.$middlewares = this.$cache.map(fn => { // 复制一份
return fn;
});
this.next(params)
}
}
export default Middleware
通用中间件使用 ajax
const middleware = new Middleware()
function transform(options, next) {
console.log('before', options.data);
options.data.age = Number(options.data.age)
next(options); // 通过验证
}
function validate(options, next) {
console.log('validate', options.data);
next(options); // 通过验证
}
function send(options, next) {
setTimeout(function () { // 模拟异步
console.log('send', options.data);
next();
}, 100);
}
middleware.use(transform).use(validate).use(send)
middleware.execute({ data: { name: 'cookie', age: '20' } });
如上我们在发送请求之前加入了类型转换、数据校验,将数据的业务处理使用中间件模式剥离,使得处理过程模块化,维护性提升。
中间件升级-事件回调
/**
* 注册事件
* @param {String} name 事件名称
* @param {Function (params)} callback 回调函数
*/
on(name, callback) {
if (typeof callback === 'function') {
this.$events[name] = callback
} else {
throw '事件回调必须为函数'
}
}
/**
* 发射(触发)事件
* @param {String} name 事件名称
* @param {Any} params 回调参数
*/
emit(name, params) {
if (this.$events[name]) {
let callback = this.$events[name]
callback.call(this, params)
} else {
throw '没有注册这个事件'
}
}
每个中间件的过程都是不可控制的,全部都交由中间类去统一调用,我们可以加入事件回调,方便我们在中间件处理过程中拥有额外的逻辑能力
将上述的使用方法再改造一下,方便实际业务中使用
function send(options, next) {
this.emit('request', options)
setTimeout(() => { // 模拟异步
console.log('send', options.data);
this.emit('response', options)
options.promise.resolve({ data: options.data })
}, 100);
}
// 请求之前的回调函数
middleware.on('request', params => {
// 在这里可以做请求之前的一些处理,比如添加全局参数等
console.log(params, '再多做一些处理')
})
// 请求成功的回调函数
middleware.on('response', params => {
// 在这里可以做下请求成功的一些处理,比如全局loading什么的
console.log(params, '请求成功')
})
middleware.use(transform).use(validate).use(send)
middleware.executeFc({ data: { name: 'cookie', age: '20' } }).then(({ data }) => {
console.log('finally', data)
});
上述的项目实例是采用 ajax 来演示,实际通用的中间件类,可以在业务中将一些流程化执行的任务拆分出来,例如表单验证、多重条件判断等等
多种条件判断
const middleware = new Middleware()
function judge1(options, next) { // 空数校验
if (!options.data) {
options.promise.reject({ data: false, msg: '数据为空' })
return
}
next(options); // 通过验证
}
function judge2(options, next) { // 判断小于10
if (options.data < 10) {
options.promise.reject({ data: false, msg: '数据小于10' })
return
}
next(options); // 通过验证
}
function judge3(options, next) { // 判断大于30
if (options.data < 30) {
options.promise.reject({ data: false, msg: '数据小于30' })
return
}
options.promise.resolve({ data: true, msg: '数据小于30' })
}
middleware.use(judge1).use(judge2).use(judge3)
middleware.executeFc({ data: 40 }).then(({ data }) => {
console.log('finally', data)
}).catch(({ msg }) => {
console.log(msg)
})
将流程化执行的多种条件判断通过中间件解耦,可以使得条件判断方法更加清晰。一般当你需要使用中介者来改造业务逻辑的时候,前端的项目确实有点复杂了。
尾声
完整的 demo 地址:项目实战 demo,喜欢的朋友可以 star 一下,后续会根据设计模式博文的推出,逐步的将此项目继续拓展出来。