职责链的定义是:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间传递,直到遇到一个可以处理它的对象,我们把这些对象称为链中的节点。

一、现实中的职责链模式

职责链的模式在现实中并不难找到,以下就是两个常见的跟职责链模式有关的场景:

  • 如果早高峰能顺利挤上公交车的话,那么估计这一天都会过的很开心。因为公交车上人实在太多了,经常上车后却找不到售票员在哪,所以只好把两块钱硬币往前面递。除非你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要在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) {
if(order Type === 1){
if(pay === true){
console.log('500元定金预购,得到100优惠券');
}else{
if(stock > 0){
console.log('普通购买,无优惠券');
}else{
console.log('手机库存不足');
}
}
}else if(order Type === 2){
if(pay === true){
console.log('200元定金预购,得到50优惠券');
}else{
if(stock > 0){
console.log('普通购买,无优惠券');
}else{
console.log('手机库存不足');
}
}
}else if(order Type === 3){
if(stock > 0){
console.log('普通购买,无优惠券');
}else{
console.log('手机库存不足');
}
}
}

虽然我们得到了意料中的运行结果,但这远远算不上一段值得夸奖的代码。order函数不仅巨大到难以阅读,而且需要经常进行修改。虽然目前项目能正常运行,但接下来的维护工作无疑是个梦麗。恐怕只有最“新手”的程序员才会写出这样的代码。

三、用职责链模式重构代码

现在我们采用职责链模式重构这段代码,先把500元订单、200元订单以及普通购买分成3个函数。

接下来把orderType、pay、stock这3个字段当作参数传递给500元订单两数,如果该函数不符合处理条件,则把这个请求传递给后面的200元订单函数,如果200元订单函数依然不能处理该请求,则继续传递请求给普通购买函数,代码如下:

// 500订单
var order500 = function(orderType, pay, stock){
if(orderType === 1 && pay === true){
console.log('500元定金预购,得到100优惠券');
}else{
order200(orderType, pay, stock); // 将请求传递给200元订单
}
};

// 200元订单
var order200 = function(orderType, pay, stock){
if(orderType === 1 && pay === true){
console.log('200元定金预购,得到50优惠券');
}else{
orderNormal(orderType, pay, stock); // 将请求传递给普通订单
}
};

// 普通购买订单
var orderNormal = function(orderType, pay, stock){
if(stock > 0){
console.log('普通购买,无优惠券');
}else{
console.log('手机库存不足');
}
};

可以看到,执行结果和前面那个巨大的order函数完全一样,但是代码的结构已经清晰了很多,我们把一个大函数拆分了3个小函数,去掉了许多嵌套的条件分支语句。

目前已经有了不小的进步,但我们不会满足于此,虽然已经把大函数拆分成了互不影响的3个小函数,但可以看到,请求在链条传递中的顺序非常僵硬,传递请求的代码被耦合在了业务函数之中:

var order500 = function(orderType, pay, stock){
if(orderType === 1 && pay === true){
console.log('500元定金预购,得到100优惠券');
}else{
order200(orderType, pay, stock); // order500 和 order200 耦合在一起
}
};

这依然是违反开放-封闭原则的,如果有天我们要增加300元预订或者去掉200元预订,意味着就必须改动这些业务函数内部。就像一根环环相扣打了死结的链条,如果要增加、拆除或者移动一个节点,就必须得先砸烂这根链条。

四、灵活拆分职责链节点

我们采用一种更灵活的方式,来改进上面的职责链模式,目标是让链中的各个节点可以灵活拆分和重组。

首先需要改写一下分别表示3种购买模式的节点函数,我们约定,如果某个节点不能处理请求,则返回一个特定的字符串 ‘nextSuccessor’来表示该请求需要继续往后面传递:

// 500订单
var order500 = function(orderType, pay, stock){
if(orderType === 1 && pay === true){
console.log('500元定金预购,得到100优惠券');
}else{
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后传递
}
};

// 200元订单
var order200 = function(orderType, pay, stock){
if(orderType === 1 && pay === true){
console.log('200元定金预购,得到50优惠券');
}else{
return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后传递
}
};

// 普通购买订单
var orderNormal = function(orderType, pay, stock){
if(stock > 0){
console.log('普通购买,无优惠券');
}else{
console.log('手机库存不足');
}
};

接下来需要把丽数包装进职责链节点,我们定义一个构造的数 Chain,在new Chain 的时候传递的参数即为需要被包装的丽数,同时它还拥有一个实例属性this.successor,表示在链中的下一个节点。

此外Chain的prototype中还有两个函数,它们的作用如下所示:

// Chain,prototype.setNextSuccessor 指定在链中的下一个节点
// Chain,prototype.passRequest 传递请求给某个节点
var Chain = function(fn){
this.fn = fn;
this.successor = null;
};

Chain,prototype.setNextSuccessor = function(sucessor){
return this.successor = successor;
};

Chain,prototype.passRequest = function(){
var ret = this.fn.apply(this, arguments);
if(ret === 'nextSuccessor'){
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
}
return ret;
}

现在把3个订单函数分别包装成职责链的节点:

var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var 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元定金购买,那我们就在该链中增加一个节点即可:

var order300 = function(){
// 具体实现
}
var chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

五、异步的职责链

在上一节的职责链模式中,我们让每个节点函数同步返回一个特定的值”nextsuccessor”,来表示是否把请求传递给下一个节点。而在现实开发中,我们经常会遇到一些异步的问题,比如我们要在节点两数中发起一个ajax异步请求,异步请求返回的结果才能决定是否继续在职贵链中passReques。

这时候让节点函数同步返回mnextsuccessr 经没有意义了,所以要给chain类再增加一个原型方法Chain.prototype.next,表示手动传递请求给职责链中的下一个节点:

Chain.prototype.next = function(){
return this.successor && this.successor.passRequest.apply(this.successor, arguments);
};

来看一个异步职责链的例子:

var fn1 = new Chain(function(){
console.log(1);
return 'nextSuccessor';
});

var fn2 = new Chain(function(){
console.log(2);
var self = this;
setTimeout(function(){
self.next();
}, 1000);
});

var fn3 = new Chain(function(){
console.log(3);
});

fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();

现在得到了一个特殊的链条,请求在链中的节点里传递,但节点有权限决定什么时候把请求交给下一个节点。可以想象,异步的职责链加上命令模式(把ajax请求封装成命令对象),我们可以很方便地创建一个异步ajax队列库。

六、小结

职责链模式可以很好的帮助我们管理代码,降低发起请求的对象和处理请求的对象之间的耦合性。职责链中的节点数量和顺序是可以自由变化的,我们可以在运行时决定链中包含哪些节点。

无论是作用域链、原型链、还是DOM节点中的事件冒泡,我们都能从中找到职责链模式的影子。职责链模式还可以和组合对象模式结合在一起,用来连接部件和父部件,或者提高组合对象的效率。