theme: smartblue
上面的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()
resChain.add(‘order500’, (_, next) => {
if (orderType === 1 && pay === true) {
console.log(‘500元定金预购,拿到100元优惠券’);
return;
}
next(); // 这里将会调用order200对应的回调函数
})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
let fn = middleware[i]
if (i === middleware.length) fn = next
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
:
export class ResChain {
/**
- 按顺序存放链的key
/
keyOrder = [];
/*- key对应的函数
/
key2FnMap = new Map();
/*- 每个节点都可以拿到的对象
*/
ctx = {}
constructor(ctx) {
this.ctx = ctx;
}add(key: string, 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);
}
}
有人会说,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
的中间件方式简直一毛一样。
目前我已经把这个工具上传到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里给个🌟,感谢。
如果有什么更好的建议,在底下留言,一起探讨。
工具链接
参考
-
https://juejin.cn/post/6844903855348514829
这是一个从 https://juejin.cn/post/7368662916151377959 下的原始话题分离的讨论话题