在现实生活中,一个事件需要经过多个对象处理是很常见的场景。例如,采购审批流程、请假流程等。公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据需要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这无疑增加了难度。
在计算机软硬件中也有相关例子,如总线网中数据报传送,每台计算机根据目标地址是否同自己的地址相同来决定是否接收;还有异常处理中,处理程序根据异常的类型决定自己是否处理该异常;
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
注意:责任链模式也叫职责链模式。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。
责任链模式是一种对象行为型模式,其主要优点如下。
其主要缺点如下。
假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:缴纳500元定金的用户可以收到100元优惠券;纳200元定金的用户可以收到50元优惠券;而没有缴纳定金的用户进入普通购买模式,没有优惠券,而且在库存不足的情况下,不一定能保证买得到。下面开始设计几个字段,解释它们的含义:
下面把上面的需求用代码实现:
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队列库。
前面的例子我们是利用了一个Chain类来把普通函数包装成职责链的节点,利用JavaScript函数式的特性,我们可以实现一种更加方便地方法来创建职责链:
Function.prototype.after = function (fn) {
const self = this
return function () {
const res = self.apply(this, arguments)
if (res === 'nextSuccessor') {
return fn.apply(this, arguments)
}
return res
}
}
const order = order500.after(order200).after(orderNormal)
order(1, true, 500) // 500元定金预购,得到100元优惠券
order(2, true, 500) // 200元定金预购,得到50元优惠券
order(3, true, 500) // 普通用户购买,无优惠券
order(1, false, 0) // 手机库存不足
使用AOP方式实现职责链简单又巧妙,但这种方式把函数叠加在一起,也增加了函数的作用域,如果链条太长,也会有一定的性能问题。
职责链模式的最大优点就是解耦了请求发送者和多个请求接收者之间的关系。其次,使用了职责链模式之后,链中的节点对象可以灵活地拆分重组,增加、删除和修改节点在链中的位置都是很容易地事。它还有一个优点就是,可以手动地指定起始节点,请求并不是一定要从链中的第一个节点开始传递。
当然,这种模式并非没有缺点,首先我们不能保证某个请求一定会被链中的节点处理,所以需要在链尾增加一个保底的接受者处理这种情况。另外职责链模式使得程序中多了一些节点对象,可能在某一次请求传递中,大部分节点并没有起作用,所以过长的职责链会带来性能的损耗。
在JavaScript中。无论是作用链、原型链,还是dom节点中的事件冒泡,我们都能从中找到职责链的影子。