第十三章 职责链模式
职责链模式的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
我们把这些对象称为链中的节点。
13.1 现实中的职责链模式
中学考试时,你遇到不会答的题,就把题目编号写在小纸条上往后传递,坐在后面的同学如果也不会答,他就会把这张纸条传递给他后面的人。这就是职责链。
职责链模式最大的优点是请求发送者只需要知道链中的第一个节点,从而弱化发送者和接收者之间的强联系。
13.2 实际开发中的职责链模式
假设我们负责一个售卖手机的电商网站,用户有三种:缴纳 500 元定金、缴纳 200 元定金、未缴纳定金。
如果缴纳了 500 元定金,那么用户必定能得到一个手机以及 100 元优惠券
如果缴纳了 200 元定金,那么用户必定得到一个手机以及 50 元优惠券
如果未缴纳定金,那么用户购买时不一定能买到,且没有优惠券
我们分别用以下字段来表示:
- orderType:定金类型(1 为 500 元定金购买者 2 为 200 元定金购买者 3 为普通购买者)
- pay:用户是否已支付定金,值为 true 或者 false。虽然用户下过 500 元定金,但是一直没付款,那么只能降级为普通购买模式
- stock:表示当前库存数量,已经支付过定金的用户可以不受限制
我们的代码如下:
js1var order = function (orderType, pay, stock) { 2 if (orderType === 1) { 3 //500元定金购买模式 4 if (pay) { 5 console.log('500元定金预购,得到100元优惠券') 6 } else { 7 if (stock > 0) { 8 console.log('普通购买,无优惠券') 9 } else { 10 console.log('手机库存不足') 11 } 12 } 13 } else if (orderType === 2) { 14 //200元定金购买模式 15 if (pay) { 16 console.log('200元定金预购,得到50元优惠券') 17 } else { 18 if (stock > 0) { 19 console.log('普通购买,无优惠券') 20 } else { 21 console.log('手机库存不足') 22 } 23 } 24 } else { 25 if (stock > 0) { 26 //普通购买模式 27 console.log('普通购买,无优惠券') 28 } else { 29 console.log('手机库存不足') 30 } 31 } 32} 33order(1, true, 500) //"500元定金预购,得到100元优惠券"
可以看到由于逻辑混乱,上面的代码非常难读。
13.3 用职责链模式重构代码
下面我们重构上面的原始代码,我们首先按照不同的 orderType 分成不同的函数。
js1var order500 = function (orderType, pay, stock) { 2 if (orderType === 1 && pay) { 3 console.log('500元定金预购,得到100元优惠券') 4 } else { 5 order200(orderType, pay, stock) //请求往下传递 order500和order200耦合了 6 } 7} 8 9var order200 = function (orderType, pay, stock) { 10 if (orderType === 2 && pay) { 11 //200元定金购买模式 12 console.log('200元定金预购,得到50元优惠券') 13 } else { 14 order0(orderType, pay, stock) //请求往下传递 order200和order0耦合了 15 } 16} 17 18var order0 = function (orderType, pay, stock) { 19 if (stock > 0) { 20 console.log('普通购买,无优惠券') 21 } else { 22 console.log('手机库存不足') 23 } 24} 25 26order500(1, true, 500) //"500元定金预购,得到100元优惠券" 27order500(2, true, 500) //"200元定金预购,得到50元优惠券" 28order500(2, false, 500) //"普通购买,无优惠券" 29order500(3, true, 500) //"普通购买,无优惠券"
上面的代码中,order500 会将请求往下传给 order200,order200 会传递给 order0。
现在我们的代码清晰许多了,但是请求在链条传递中的顺序非常僵硬,传递请求也被耦合在业务函数中。
这是违反开放-封闭原则的,如果有一天我们要增加业务逻辑,那么必须改动这些业务函数的内部。
软件工程中,首先应该解决的是重复,其次是维护性。
我们应当让上面的代码在业务发生巨大变动后尽量少地改动。
13.4 灵活可拆分的职责链节点
我们可以看到,当 order500 和 order200 都进入 else 判断时,就会往下传递。那我们只需要改写这段代码。
我们在写逻辑时,内部约定只要不成立,就返回 next 表示进入下一个函数。
js1var order500 = function (orderType, pay, stock) { 2 if (orderType === 1 && pay) { 3 console.log('500元定金预购,得到100元优惠券') 4 } else { 5 return 'next' //不管后面的逻辑,这里就是返回next 6 } 7} 8 9var order200 = function (orderType, pay, stock) { 10 if (orderType === 2 && pay) { 11 //200元定金购买模式 12 console.log('200元定金预购,得到50元优惠券') 13 } else { 14 return 'next' 15 } 16} 17 18var order0 = function (orderType, pay, stock) { 19 if (stock > 0) { 20 console.log('普通购买,无优惠券') 21 } else { 22 console.log('手机库存不足') 23 } 24}
接下来我们需要将函数包装进一些对象中,这些对象中包含当前要处理的函数、下一步要处理什么函数以及处理这些函数的属性。我们的职责链就是通过这些对象来连接的。
js1{ 2 currentExecute:fn,//保存当前步骤的函数 3 nextStep:xxx, // 保存下一步步骤的函数 表示下一个xxx节点 4 start:fn // 开始处理当前步骤和下一步步骤的函数 5}
我们采用 class 构造函数的形式来构造这些对象
js1class Step { 2 constructor(fn) { 3 this.currentExecute = fn 4 this.nextStep = null 5 } 6 setNextStep(step) { 7 return (this.nextStep = step) 8 } 9 start(...rest) { 10 const result = this.currentExecute.call(this, ...rest) 11 if (result === 'next') { 12 return this.nextStep && this.nextStep.start.call(this.nextStep, ...rest) 13 } 14 return result 15 } 16}
然后使用这个构造函数来构造步骤,并且给每个对象都设置下一步需要做什么的函数
js1const step1 = new Step(order500) 2const step2 = new Step(order200) 3const step3 = new Step(order0) 4//设置下一步需要做什么 5step1.setNextStep(step2).setNextStep(step3)
最后通过 start 来启动步骤
js1step1.start(3, false, 0) //"手机库存不足" 2step1.start(3, false, 30) //"普通购买,无优惠券"
上面的代码逻辑已经非常清晰了,我们分别使用 Step 构造函数来构造 step 对象,然后调用 step 对象内部的 setNextStep 来让他们互相连接起来,最后只需要通过第一步的 start 函数就可以运行整个步骤。
通过改进,我们就可以非常灵活地增加、移除、修改链中的顺序。
13.5 异步的职责链
我们让每个节点函数同步返回一个特定的 next,来表示将请求传递给下一个节点。在实际开发中,我们可能会遇到异步的问题,可能需要在节点函数中发起一个 ajax 异步请求,异步请求返回的结果来决定是否要继续在职责链中,这样的话 return 一个特定的字符串就没有意义了。
有没有可能让整个过程更加优雅呢?
比如类似于 koa,通过调用 next 来实现“跳转”的效果。
我们可以封装一个 step.prototype.next 函数,然后交由 currentExecute 中保存的逻辑函数来自行调用。
js1class Step { 2 constructor(fn) { 3 this.currentExecute = fn 4 this.nextStep = null 5 } 6 setNextStep(step) { 7 return (this.nextStep = step) 8 } 9 //这里是封装的next 10 next(...rest) { 11 return this.nextStep && this.nextStep.start.call(this.nextStep, ...rest) 12 } 13 start(...rest) { 14 const result = this.currentExecute.call(this, ...rest) 15 return result 16 } 17}
逻辑函数则可以通过this.next来自行调用
js1var order500 = function (orderType, pay, stock) { 2 if (orderType === 1 && pay) { 3 console.log('500元定金预购,得到100元优惠券') 4 } else { 5 this.next(orderType, pay, stock) //通过this来调用 6 } 7} 8 9var order200 = function (orderType, pay, stock) { 10 const self = this 11 if (orderType === 2 && pay) { 12 //200元定金购买模式 13 console.log('200元定金预购,得到50元优惠券') 14 } else { 15 setTimeout(function () { 16 self.next(orderType, pay, stock) //setTimeout中的this变成了window,所以需要改写 17 }) 18 } 19} 20 21var order0 = function (orderType, pay, stock) { 22 if (stock > 0) { 23 console.log('普通购买,无优惠券') 24 } else { 25 console.log('手机库存不足') 26 } 27}
现在我们得到了一个特殊的链条,请求在链中的节点里传递,节点有权利决定什么时候把请求给下一个节点。
上面的代码还可以再优化,我们发现 this 的指向需要交由写逻辑函数的人特意指定,
js1self.next(orderType, pay, stock) //setTimeout中的this变成了window,所以需要改写
如果写逻辑函数的人不知道你对于 Step 类做的细节,那么很有可能就出错了,实际上我们可以将 next 函数通过参数的形式传递给逻辑函数。
js1class Step { 2 constructor(fn) { 3 this.currentExecute = fn 4 this.nextStep = null 5 } 6 setNextStep(step) { 7 return (this.nextStep = step) 8 } 9 next(...rest) { 10 const next = this.nextStep.next.bind(this.nextStep) 11 return this.nextStep && this.nextStep.start.apply(this.nextStep, [...rest, next]) //这里直接传递给下一个节点的函数 12 } 13 start(...rest) { 14 const next = this.next.bind(this) 15 const result = this.currentExecute.apply(this, [...rest, next]) //这里传递给当前要调用的函数 16 return result 17 } 18}
现在就可以通过参数取出 next 了。
js1var order500 = function (orderType, pay, stock, next) { 2 if (orderType === 1 && pay) { 3 console.log('500元定金预购,得到100元优惠券') 4 } else { 5 next(orderType, pay, stock) 6 } 7} 8 9var order200 = function (orderType, pay, stock, next) { 10 const self = this 11 if (orderType === 2 && pay) { 12 //200元定金购买模式 13 console.log('200元定金预购,得到50元优惠券') 14 } else { 15 next(orderType, pay, stock) 16 } 17} 18 19var order0 = function (orderType, pay, stock, next) { 20 if (stock > 0) { 21 console.log('普通购买,无优惠券') 22 } else { 23 console.log('手机库存不足') 24 } 25}
类似 koa 的洋葱模型已经完成了。
13.6 职责链模式的优缺点
职责链模式最大的优点就是解耦了请求发送者和 N 个接收者之间的复杂关系,只需要把请求传递给第一个节点即可。
在职责链前我们的逻辑是这样的
使用职责链改进后后我们的业务代码逻辑就是这样的
使用职责链后,让每种订单都有各自处理的函数而不互相影响。
其次,使用了职责链模式后,链中的节点对象可以灵活拆分重组。增加或者删除一个节点,或者改变节点在链中的位置都可是轻而易举的事情。
职责链模式还有一个优点,我们可以手动指定开始的节点,请求并不是非得从链中的第一个节点开始的。
如果运用得当,职责链模式可以很好地帮助我们组织代码。
职责链模式的缺点在于使得程序中多了一些节点对象,可能在某一次请求传递的过程中,大部分的节点都没有实际的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能耗损。
13.7 用 AOP 实现职责链
使用 AOP 的方式比 class 方式更加方便直观,假设我们依然约定每个函数的等式不成立时,返回一个 next
js1var order500 = function (orderType, pay, stock) { 2 if (orderType === 1 && pay) { 3 console.log('500元定金预购,得到100元优惠券') 4 } else { 5 return 'next' 6 } 7} 8 9var order200 = function (orderType, pay, stock) { 10 if (orderType === 2 && pay) { 11 //200元定金购买模式 12 console.log('200元定金预购,得到50元优惠券') 13 } else { 14 return 'next' 15 } 16} 17 18var order0 = function (orderType, pay, stock) { 19 if (stock > 0) { 20 console.log('普通购买,无优惠券') 21 } else { 22 console.log('手机库存不足') 23 } 24}
然后我们用一个 after 函数来实现切面,当函数返回 next 时,就会进入下一个函数
js1Function.prototype.after = function (fn) { 2 const self = this 3 return function (...rest) { 4 const result = self.apply(this, rest) 5 if (result === 'next') { 6 return fn.apply(this, rest) 7 } 8 return result 9 } 10} 11 12const order = order500.after(order200).after(order0) 13order(3, false, 500) //"普通购买,无优惠券" 14order(2, true, 500) //"200元定金预购,得到50元优惠券"
用 AOP 来实现职责链既简单又巧妙。
13.9 小结
职责链模式可以很好地帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
无论是作用域链、原型链、还是 DOM 节点的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合模式结合在一起,用来连接部件和父部件,或是提高组合对象的效率。