责任链模式最强工具res-chain🚀


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嵌套,每个链的职责分,我们可以看出责任链模式存在的优点:

  1. 降低了代码之间的耦合,很好的对每个处理逻辑进行封装。在每个链条内,只需要关注自身的逻辑实现。
  2. 增强了代码的可维护性。我们可以很轻易在原有链条内的任何位置添加新的节点,或者对链条内的节点进行替换或者删除。

责任链还特别的灵活,如果说后面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里给个🌟,感谢。

如果有什么更好的建议,在底下留言,一起探讨。

工具链接

res-chain

参考

  • koa-compose

  • https://juejin.cn/post/6844903855348514829


这是一个从 https://juejin.cn/post/7368662916151377959 下的原始话题分离的讨论话题