JavaScript设计模式之职责链模式
定义
职责链模式的定义:使多个对象都有机会处理请求,从而避免了请求的发送者与多个接收者直接的耦合关系,将这些接收者连接成一条链,顺着这条链传递该请求,直到找到能处理该请求的对象。
应用
假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:缴纳500元定金的用户可以收到100元优惠券;纳200元定金的用户可以收到50元优惠券;而没有缴纳定金的用户进入普通购买模式,没有优惠券,而且在库存不足的情况下,不一定能保证买得到。下面开始设计几个字段,解释它们的含义:
- orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。
- pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。
- stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。
下面把上面的需求用代码实现:
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元优惠券'
复制代码
虽然通过上面代码我们得到了想要的结果,但是代码难以阅读,维护起来也很困难,如果需要修改需求,那代价无疑是巨大的。
使用职责链模式重构
下面我们使用职责链模式重构,先把500元订单、200元订单以及普通购买拆分成三个函数。代码如下:
function order500 (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
order200(orderType, pay, stock)
}
}
function order200 (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
order200(orderType, pay, stock)
}
}
function orderNormal (orderType, pay, stock) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
// 测试
order500(1, true, 500) // 500元定金预购,得到100元优惠券
order500(1, false, 500) // 普通用户购买,无优惠券
order500(2, true, 500) // 200元定金预购,得到50元优惠券
order500(3, false, 500) // 普通用户购买,无优惠券
order500(3, false, 0) // 手机库存不足
复制代码
可以看到,重构后的代码已经清晰很多,减少了大量的if-else嵌套,每个函数的职责分明。但是还不够,虽然我们把大函数拆分成了三个小函数,但是请求在链条中传递的顺序很僵硬,传递请求的代码跟业务代码耦合在一起,如果有一天要增加300元定金的预订,那么就要切断之前的链条,修改订单500函数的代码,重新在500和200之间加一根新的链条,这违反了开放-封闭原则。
灵活可拆分的职责链节点
首先修改三个函数,如果某个节点不能处理请求,则返回一个特定的字符串“nextSuccessor”来表示请求需要继续往后传递:
function order500 (orderType, pay, stock) {
if (orderType === 1 && pay === true) {
console.log('500元定金预购,得到100元优惠券')
} else {
return 'nextSuccessor'
}
}
function order200 (orderType, pay, stock) {
if (orderType === 2 && pay === true) {
console.log('200元定金预购,得到50元优惠券')
} else {
return 'nextSuccessor'
}
}
function orderNormal (orderType, pay, stock) {
if (stock > 0) {
console.log('普通用户购买,无优惠券')
} else {
console.log('手机库存不足')
}
}
复制代码
接下来需要定义一个Chain类将三个函数包装进职责链节点:
class Chain {
construct (fn) {
this.fn = fn
this.successor = null
}
setNextSuccessor (successor) {
return this.successor = successor
}
passRequest () {
const res = this.fn.apply(this, arguments)
if (res === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
return res
}
}
// 包装三个订单函数
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const chainOrderNormal = new Chain(orderNormal)
// 指定节点在职责链中的位置
chainOrder500.setNextSuccessor(chainOrder200)
chainOrder200.setNextSuccessor(chainOrderNormal)
// 最后把请求传递给第一个节点
chainOrder500.passRequest(1, true, 500) // 500元定金预购,得到100元优惠券
chainOrder500.passRequest(2, true, 500) // 200元定金预购,得到50元优惠券
chainOrder500.passRequest(3, true, 500) // 普通用户购买,无优惠券
chainOrder500.passRequest(1, false, 0) // 手机库存不足
复制代码
改进之后的代码,我们可以灵活地增加、移除和修改链中的节点顺序,如果后面增加了300预定金的类型,只需要在链中增加一个节点:
function order300 () {
// 省略代码
}
const chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)
复制代码
这样的修改简单容易,完全不用理会原来其它订单的代码。
异步的职责链
在上面的例子中,每个节点函数都是同步返回一个特定值来表示是否把请求传递给下一个节点。但是在实际应用中,我们经常会遇到一些异步的问题,比如要在某个节点中通过发起一个ajax异步请求,需要根据异步请求返回的结果才决定是否继续传递请求,这时候我们需要再添加一个函数,手动传递请求给职责链中的下一个节点:
class Chain {
construct (fn) {
this.fn = fn
this.successor = null
}
setNextSuccessor (successor) {
return this.successor = successor
}
next () {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
passRequest () {
const res = this.fn.apply(this, arguments)
if (res === 'nextSuccessor') {
return this.successor && this.successor.passRequest.apply(this.successor, arguments)
}
return res
}
}
复制代码
看一个异步使用的例子:
const fn1 = new Chain(function () {
console.log(1)
return 'nextSuccessor'
})
const fn1 = new Chain(function () {
console.log(2)
setTimeout(() => {
this.next()
}, 1000)
})
const fn3 = new Chain(function () {
console.log(3)
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)
fn1.passRequest()
复制代码
这样我们得到了一个可以处理异步情况的职责链,异步的职责链加上命令模式,可以很方便地创建一个异步ajax队列库。
什么时候使用责任链模式?
当你负责的模块,基本满足以下情况时
- 你负责的是一个完整流程,或你只负责流程中的某个环节
- 各环节可复用
- 各环节有一定的执行顺序
- 各环节可重组
责任链的优缺点
优点:
- 符合单一职责,使每个方法中都只有一个职责。
- 符合开放封闭原则,在需求增加时可以很方便的扩充新的责任。
- 使用时候不需要知道谁才是真正处理方法,减少大量的
if或switch语法。
缺点:
- 团队成员需要对责任链存在共识,否则当看到一个方法莫名其妙的返回一个 next 时一定会很奇怪。
- 出错时不好排查问题,因为不知道到底在哪个责任中出的错,需要从链头开始往后找。
- 就算是不需要做任何处理的方法也会执行到,因为它在同一个链中,文中的例子都是同步执行的,如果有异步请求的话,执行时间也许就会比较长。
与策略模式的不同
在前面我还提到过策略模式,先说说两个模式之间的相似处,那就是都可以替多个同一个行为(response400、response401 等)定义一个接口(httpErrorHandler),而且在使用时不需要知道最后是谁执行的。在实现上策略模式比较简单。
由于策略模式直接用 if 或 switch 来控制谁该做这件事情,比较适合一个萝卜一个坑的状况。而策略模式虽然在例子中也是针对错误的状态码做各自的事,都在不归自己管的时候直接把事交给下一位处理,但是在责任链中的每个节点仍然可以在不归自己管的时候先做些什么,然后再交给下个节点:
const response400 = (error) => {
if (error.response.status !== 400) {
// 先做点什么...
return 'next';
}
console.log('你是不是提交了什么奇怪的东西?');
};
总结
职责链模式的最大优点就是解耦了请求发送者和多个请求接收者之间的关系。其次,使用了职责链模式之后,链中的节点对象可以灵活地拆分重组,增加、删除和修改节点在链中的位置都是很容易地事。它还有一个优点就是,可以手动地指定起始节点,请求并不是一定要从链中的第一个节点开始传递。
当然,这种模式并非没有缺点,首先我们不能保证某个请求一定会被链中的节点处理,所以需要在链尾增加一个保底的接受者处理这种情况。另外职责链模式使得程序中多了一些节点对象,可能在某一次请求传递中,大部分节点并没有起作用,所以过长的职责链会带来性能的损耗。
在JavaScript中。无论是作用链、原型链,还是DOM节点中的事件冒泡,我们都能从中找到职责链的影子。

浙公网安备 33010602011771号