职责链模式
职责链的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。
一、现实中的职责链模式
职责链的模式在现实中并不难找到,以下就是两个常见的跟职责链模式有关的场景:
- 如果早高峰能顺利挤上公交车的话,那么估计这一天都会过的很开心。因为公交车上人实在太多了,经常上车后却找不到售票员在哪,所以只好把两块钱硬币往前面递。除非你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要在N个人手上传递,才能最终到达售票员的手里。
- 中学时代的期末考试,如果你平时不太老实,考试时就会被安排在第一个位置。遇到不会答的题目,就把题目编号写在小纸条上往后传递,坐在后面的同学如果也不会答,他就会把这张小纸条继续递给他后面的人。
从这两个例子中,我们很容易找到职责链模式的最大优点:请求发送者只需要知道链中第一个节点,从而弱化了发送者和一组接收者之间的强联系。如果不使用职责链模式,那么在公车上,我就得先搞清楚谁是售票员,才能把硬币遂给他。同样,在期末考试中,也许我就要先了解同学中有哪些可以解答这道题。
二、实际开发中的职责链模式
假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后(订单已在此时生成),现在已经到了正式购买的阶段。
公司针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过500元定金的用户会收到100元的商城优惠券,200元定金的用户可以收到50元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。
我们的订单页面是PIP吐出的模板,在页面加载之初,PTIP会传递给页面几个字段。
- orderType:表示订单类型(定金用户或者普通购买用户)code的值为1的时候是500元定金用户,为2的时候是200元定金用户,为3的时候是普通购买用户。
- pay:表示用户是否已经支付定金,值为true或者false,虽然用户已经下过500元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
- stock:表示当前用于普通购买的手机库存数量,已经支付过500元或者200元定金的用户不受此限制。
下面我们把这个流程写成代码:
var order = function(orderType, pay, stock) { |
虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。order函数不仅巨大到难以阅读,而且需要经常进行修改。虽然目前项目能正常运行,但接下来的维护工作无疑是个梦麗。恐怕只有最“新手”的程序员才会写出这样的代码。
三、用职责链模式重构代码
现在我们采用职责链模式重构这段代码,先把500元订单、200元订单以及普通购买分成3个函数。
接下来把orderType、pay、stock这3个字段当作参数传递给500元订单两数,如果该函数不符合处理条件,则把这个请求传递给后面的200元订单函数,如果200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数,代码如下:
// 500订单 |
可以看到,执行结果和前面那个巨大的order函数完全一样,但是代码的结构已经清晰了很多,我们把一个大函数拆分了3个小函数,去掉了许多嵌套的条件分支语句。
目前已经有了不小的进步,但我们不会满足于此,虽然已经把大函数拆分成了互不影响的3个小函数,但可以看到,请求在链条传递中的顺序非常僵硬,传递请求的代码被耦合在了业务函数之中:
var order500 = function(orderType, pay, stock){ |
这依然是违反开放-封闭原则的,如果有天我们要增加300元预订或者去掉200元预订,意味着就必须改动这些业务函数内部。就像一根环环相扣打了死结的链条,如果要增加、拆除或者移动一个节点,就必须得先砸烂这根链条。
四、灵活拆分职责链节点
我们采用一种更灵活的方式,来改进上面的职责链模式,目标是让链中的各个节点可以灵活拆分和重组。
首先需要改写一下分别表示3种购买模式的节点函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串 ‘nextSuccessor’来表示该请求需要继续往后面传递:
// 500订单 |
接下来需要把丽数包装进职责链节点,我们定义一个构造的数 Chain,在new Chain 的时候传递的参数即为需要被包装的丽数,同时它还拥有一个实例属性this.successor,表示在链中的下一个节点。
此外Chain的prototype中还有两个函数,它们的作用如下所示:
// Chain,prototype.setNextSuccessor 指定在链中的下一个节点 |
现在把3个订单函数分别包装成职责链的节点:
var chainOrder500 = new Chain(order500); |
然后指定在职责链中的顺序:
chainOrder500.setNextSuccessor(chainOrder200); |
最后把请求传递给第一个节点:
chainOrder500.passRequest(1,true,500); // 输出:500元定金预购,得到100优惠券 |
通过改进,我们可以自由灵活地增加,移除和修改链中地节点顺序,假如某天网站运营人员又想出了支持300元定金购买,那我们就在该链中增加一个节点即可:
var order300 = function(){ |
五、异步的职责链
在上一节的职责链模式中,我们让每个节点函数同步返回一个特定的值”nextsuccessor”,来表示是否把请求传递给下一个节点。而在现实开发中,我们经常会遇到一些异步的问题,比如我们要在节点两数中发起一个ajax异步请求,异步请求返回的结果才能决定是否继续在职贵链中passReques。
这时候让节点函数同步返回mnextsuccessr 经没有意义了,所以要给chain类再增加一个原型方法Chain.prototype.next,表示手动传递请求给职责链中的下一个节点:
Chain.prototype.next = function(){ |
来看一个异步职责链的例子:
var fn1 = new Chain(function(){ |
现在得到了一个特殊的链条,请求在链中的节点里传递,但节点有权限决定什么时候把请求交给下一个节点。可以想象,异步的职责链加上命令模式(把ajax请求封装成命令对象),我们可以很方便地创建一个异步ajax队列库。
六、小结
职责链模式可以很好的帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。
无论是作用域链、原型链、还是DOM节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合对象模式结合在一起,用来连接部件和父部件,或者提高组合对象的效率。