责任链模式最强工具res-chain🚀
上面的logo是由ai生成
责任链模式介绍
责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,它通过把请求的发送者和接收者解耦,将多个对象连接成一个链,并沿着这条链传递请求,直到有一个对象能够处理它为止,从而避免了请求的发送者和接收者之间的直接耦合。
在责任链模式中,每个处理者都持有对下一个处理者的引用,即构成一个链表结构。当请求从链头开始流经链上的每个处理者时,如果某个处理者能够处理该请求,就直接处理,否则将请求发送给下一个处理者,直到有一个处理者能够处理为止。
这种方式可以灵活地动态添加或修改请求的处理流程,同时也避免了由于请求类型过多而导致类的爆炸性增长的问题。
看完以上责任链的描述,有没有发现跟Node.js的某些库特别的像,没错,就是koa
。什么?没用过koa
?那我建议你立马学起来,因为它用起来特别的简单。
下面来一个简单使用koa
的例子:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
}
next(); // 执行下面的回调函数
});
app.use(async (ctx, next) => {
if (ctx.request.url === '/hello') {
ctx.body = 'hello world';
return;
}
});
app.listen(3000);
通过node运行上面的代码,在浏览器请求localhost:3000
,接口就会返回home
,当我们请求localhost:3000/hello
,接口就会返回hello world
。
上面对请求的处理过程就很符合职责链模式的思想,我们可以清楚的知道每个链做的工作,并且链条的顺序流程也很清晰。
有人就会问,只在一个回调里面也能处理呀,比如下面的代码:
app.use(async (ctx, next) => {
if (ctx.request.url === '/') {
ctx.body = 'home';
return;
} else if (ctx.request.url === '/home') {
ctx.body = 'hello world';
return
}
});
是的,上面的代码确实可以实现,但是这就要回到我们使用责任链模式的初衷了:为了逻辑解耦。
责任链解决的问题
我们继续接着聊上一节的问题,使用if
确实可以实现相同效果,但是在某些场景中,if
并没有职责链那么好用,为什么这么说呢。
我们找一个应用案例举个例子:
假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:
- 缴纳500元定金的用户可以收到100元优惠券;
- 缴纳200元定金的用户可以收到50元优惠券;
- 没有缴纳定金的用户进入普通购买模式,没有优惠券。
- 而且在库存不足的情况下,不一定能保证买得到。
下面开始设计几个字段,解释它们的含义:
orderType
:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。pay
:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。stock
:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。
下面我们分别用if
和职责链模式来实现:
使用if:
const order = function (orderType, pay, stock) {
if (orderType === 1) {
if (pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
} else if (orderType === 2) {
if (pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
} else if (orderType === 3) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
}
order(1, true, 500) // 输出:500元定金预购,得到100元优惠券'
虽然上面的代码也可以实现需求,但是代码实在是难以阅读,维护起来更是困难,如果继续在这个代码上开发,未来肯定会成为一座很大的屎山。
下面我们使用责任链模式来实现:
function printResult(orderType, pay, stock) {
// 这里ResChain类是模拟koa的写法,后面会讲如何实现ResChain
// 请先耐心看完它是如何处理的
const resChain = new ResChain();
// 针对500元定金的情况
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next(); // 这里将会调用order200对应的回调函数
});
// 针对200元定金的情况
resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next(); // 这里会调用noOrder对应回调函数
});
// 针对普通用户购买的情况
resChain.add('noOrder', (_, next) => {
if (stock > 0) {
console.log('普通用户购买,无优惠券');
} else {
console.log('手机库存不足');
}
});
resChain.run(); // 开始执行order500对应的回调函数
}
// 测试
printResult(1, true, 500); // 500元定金预购,得到100元优惠券
printResult(1, false, 500); // 普通用户购买,无优惠券
printResult(2, true, 500); // 200元定金预购,得到50元优惠券
printResult(3, false, 500); // 普通用户购买,无优惠券
printResult(3, false, 0); // 手机库存不足
以上的代码经过责任链处理之后特别的清晰,并且减少了大量的if-else
嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:
- 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。
- 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。
责任链还特别的灵活,如果说后面pm找我们加需求,需要加多一个预付定金400,返回80元优惠券,处理起来也是易如反掌,只需要怼回去,只需要在order500
下面加多一个节点处理即可:
...
resChain.add('order500', (_, next) => {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,拿到100元优惠券');
return;
}
next();
})
+ // 加上这一块
+ resChain.add('order400', (_, next) => {
+ if (orderType === 3 && pay === true) {
+ console.log('400元定金预购,拿80元优惠券');
+ return;
+ }
+ next();
+ })
resChain.add('order200', (_, next) => {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,拿到50元优惠券');
return;
}
next();
})
...
就是这么简单。那这个ResChain
是如何实现的呢?
封装ResChain
先别急,首先我们来了解一下koa
是如何实现:在链节点的回调函数内调用next
就可以跳到下一个节点的呢?
话不多说,直接看源码,参考的库是koa-compose,代码也是特别的简洁:
function compose (middleware) {
// 这里传入的middleware是函数数组,例如: [fn1, fn2, fn3, fn4, ...]
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
// 判断数组里的元素是不是函数类型
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0);
// 这里利用了函数申明提升的特性
function dispatch (i) {
// 这里是防止重复调用next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 从middleware中取出回调函数
let fn = middleware[i]
if (i === middleware.length) fn = next
// 如果fn为空了,则结束运行
if (!fn) return Promise.resolve()
try {
// next函数其实就是middleware的下一个函数,执行next就是执行下一个函数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
看完源代码,我们接着来实现ResChain
类,首先整理一下应该要有的方法:
add
方法。可以添加回调函数,并按添加的顺序执行。run
方法。开始按顺序执行责任链。
当add
方法执行的时候,把回调函数按顺序push进一个数组中。
export class ResChain {
/**
* 按顺序存放链的key
*/
keyOrder = [];
/**
* key对应的函数
*/
key2FnMap = new Map();
/**
* 每个节点都可以拿到的对象
*/
ctx = {}
constructor(ctx) {
this.ctx = ctx;
}
// 这里用key来标识当前callback的唯一性,后面重复添加可以区分。
add(key, callback) {
if (this.key2FnMap.has(key)) {
throw new Error(`Chain ${key} already exists`);
}
this.keyOrder.push(key);
this.key2FnMap.set(key, callback);
return this;
}
async run() {
let index = -1;
const dispatch = (i) => {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'));
}
index = i;
const fn = this.key2FnMap.get(this.keyOrder[i]);
if (!fn) {
return Promise.resolve(void 0);
}
return fn(this.ctx, dispatch.bind(null, i + 1));
};
return dispatch(0);
}
}
add
方法的第一个参数key
可以用来判断是否已经添加过相同的回调。
有人会说,koa
的中间件是异步函数的,你这个行不行?
当然可以,接下来看个异步的例子:
const resChain = new ResChain();
resChain.add('async1', async (_, next) => {
console.log('async1');
await next();
});
resChain.add('async2', async (_, next) => {
console.log('async2')
// 这里可以执行一些异步处理函数
await new Promise((resolve, reject) => {
setTimeOut(() => {
resolve();
}, 1000)
});
await next();
});
resChain.add('key3', async (_, next) => {
console.log('key3');
await next();
});
// 执行责任链
await resChain.run();
console.log('finished');
// 先输出 async1 async2 然后停顿了1秒钟之后,才输出async3 finished
🚧 需要注意:如果是异步模式,则链上的每个回调函数必须要 await next(),因为next函数代表下一个环的异步函数。
跟koa
的中间件方式简直一毛一样。
有人可能还注意到了,ResChain
实例化的时候可以传入对象,比如下面的代码:
const resChain = new ResChain({ interrupt: false });
传入对象具体有什么用法呢?可以用来获取一些在链中处理好的数据,来实现发送者和处理者的解耦。可能比较抽象,我们来举个例子。
比如需要进行数据校验的场景,如果不通过,则中断提交:
const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}
const resChain = new ResChain(ctx);
resChain.add('校验name', (ctx, next) => {
const { name = '' } = ctx;
if (name === '') {
ctx.error = '请填写name';
ctx.interrupt = true;
return;
}
next();
})
resChain.add('校验phone', (ctx, next) => {
const { phone = '' } = ctx;
if (phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
return;
}
next();
})
// 执行责任链
resChain.run();
// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}
如果是使用if来实现:
const ctx = {
// 表单项
model: {
name: '',
phone: '',
},
// 错误提示
error: '',
// 是否中断
interrupt: false,
}
if(ctx.model.name === '') {
ctx.error = '请填写用户名';
ctx.interrupt = true;
}
if (!ctx.interrupt && ctx.model.phone === '') {
ctx.error = '请填写手机号';
ctx.interrupt = true;
}
// 如果需要中断,则提示
if (resChain.ctx.interrupt) {
alert(resChain.ctx.error);
return;
}
可以发现,对phone
的判断逻辑,就要先判断interrupt
是否为false
才能继续,而且如果下面还有其他的字段校验,那必须都走一遍if。
这也是责任链的一个优势,可以在某个环节按自己的想法停止,不用继续走后面的节点。
目前我已经把这个工具上传到npm
了,如果想要在自己的项目中使用,直接安装:
res-chain即可使用:
npm install res-chain
# 或者
# yarn add res-chain
引入:
import { ResChain } from 'res-chain';
// CommonJS方式的引入也是支持的
// const { ResChain } = 'res-chain';
const resChain = new ResChain();
resChain.add('key1', (_, next) => {
console.log('key1');
next();
});
resChain.add('key2', (_, next) => {
console.log('key2');
// 这里没有调用next,则不会执行key3
});
resChain.add('key3', (_, next) => {
console.log('key3');
next();
});
// 执行职责链
resChain.run(); // => 将会按顺序输出 key1 key2
芜湖起飞🚀。
有了这个工具函数,我们就可以视场景去优化项目中的一大坨if-else嵌套,或者直接使用它来实现一些业务中比较复杂的逻辑。
起源
这个工具诞生的过程还挺巧合的,某一天周六我在公司加班赶需求,发现需要在一堆旧逻辑if-else中添加新的逻辑,强迫症的我实在是无法忍受在💩山上继续堆💩。。。
我陷入沉思,用什么方式去优化呢?看了网上责任链模式的实现,感觉还是不够优雅。
无意中翻到了之前用koa
写的项目,突然灵光乍现💡,koa
的中间件不就是一个很棒的实践。调用next
就能够往下一个节点走,不调用的话就可以终止。
于是立即动工,三下五除二就完成了。我还推荐给部门的其他前端小伙伴,他们也在一些需求的复杂逻辑中有运用。
总结
过去无意学到的某个知识,或者某个概念,在未来也许会发挥作用,你只需要做的就是等待。
没有koa
这么棒的库,估计也不会有这工具了,所以还是得感谢它的作者如此聪明。😁
如果你也喜欢这个工具,欢迎去github里给个🌟,感谢。
如果有什么更好的建议,在底下留言,一起探讨。
工具链接
参考
来源:juejin.cn/post/7368662916151377959