JavaScript 设计模式之职责链模式

1,387 阅读9分钟

这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战

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

现实中的职责链模式

  1. 高峰期间坐公交车的车票问题,需要经过N个人手上传递才可以到达售票员的手里。
  2. 考试时,遇到不会答的题目,就把题目编号写在小纸条上往后传递,坐在后面的同学如果也不会答,他就会把这张小纸条继续递给他后面的人。

从这两个示例可以找到职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。

实际开发中的职责链模式

促销时针对支付过定金的用户有一定的优惠政策。在正式购买后,已经支付过 500 元定金的用户会收到 100 元的商城优惠券,200 元定金的用户可以收到 50 元的优惠券,而之前没有支付定金的用户只能进入普通购买模式,也就是没有优惠券,且在库存有限的情况下不一定保证能买到。

接收一下三个参数:

  • orderType:表示订单类型(定金用户或者普通购买用户),code 的值为 1 的时候是 500 元定金用户,为 2 的时候是 200 元定金用户,为 3 的时候是普通购买用户。
  • pay:表示用户是否已经支付定金,值为 true 或者 false, 虽然用户已经下过 500 元定金的订单,但如果他一直没有支付定金,现在只能降级进入普通购买模式。
  • stock:表示当前用于普通购买的手机库存数量,已经支付过 500 元或者 200 元定金的用户不受此限制。

接下来把这个流程写成代码:

var order = function (orderType, pay, stock) {
  if (orderType === 1) { // 500 元定金购买模式
    if (pay === true) { // 已支付定金
      console.log('500 元定金预购, 得到 100 优惠券');
    } else { // 未支付定金,降级到普通购买模式
      if (stock > 0) { // 用于普通购买的手机还有库存
        console.log('普通购买, 无优惠券');
      } else {
        console.log('手机库存不足');
      }
    }
  }
  else if (orderType === 2) {
    if (pay === true) {
      // 200 元定金购买模式
      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 元订单
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 === 2 && 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('手机库存不足');
  }
};
// 测试结果:
order500(1, true, 500); // 输出:500 元定金预购, 得到 100 优惠券
order500(1, false, 500); // 输出:普通购买, 无优惠券
order500(2, true, 500); // 输出:200 元定金预购, 得到 500 优惠券
order500(3, false, 500); // 输出:普通购买, 无优惠券
order500(3, false, 0);// 输出:手机库存不足

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

但是,还有不足之处,可以看到请求在链条中的顺序非常僵硬,传递请求的代码被耦合在了业务函数中。这依然是违反开放-封闭原则的,如果有一天要增加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 === 2 && 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,表示在链中的下一个节点。

var Chain = function (fn) {
  this.fn = fn;
  this.successor = null;
};
// 指定在链中的下一个节点
Chain.prototype.setNextSuccessor = function (successor) {
  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 () {
    // 具体实现略
};
chainOrder300 = new Chain(order300); 
chainOrder500.setNextSuccessor(chainOrder300); 
chainOrder300.setNextSuccessor(chainOrder200);

异步的职责链

在上述的职责链模式中,我们让每个节点函数同步返回一个特定的值 nextSuccessor ,来表示是否把请求传递给下一个节点。而在现实开发中,我们经常会遇到一些异步的问题,比如我们要在节点函数中发起一个 ajax 异步请求,异步请求返回的结果才能决定是否继续在职责链中 passRequest。此时,让节点函数同步返回 nextSuccessor 已经没有意义了,所以要给 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();

职责链模式的优缺点

优点

  1. 解耦了请求发送者和 N 个接收者之间的复杂关系,由于不知道链中的哪个节点可以处理你发出的请求,所以你只需把请求传递给第一个节点即可。
  2. 链中的节点对象可以灵活地拆分重组。增加或者删除一个节点,或者改变节点在链中的位置都是轻而易举的事情。
  3. 可以手动指定起始节点,请求并不是非得从链中的第一个节点开始传递。

缺点

  1. 不能保证某个请求一定会被链中的节点处理。在这种情况下,我们可以在链尾增加一个保底的接受者节点来处理这种即将离开链尾的请求。
  2. 职责链模式使得程序中多了一些节点对象,可能在某一次的请求传递过程中,大部分节点并没有起到实质性的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能损耗。

用 AOP 实现职责链

Function.prototype.after = function (fn) {
  var self = this;
  return function () {
    var ret = self.apply(this, arguments);
    if (ret === 'nextSuccessor') {
      return fn.apply(this, arguments);
    }
    return ret;
  }
};
var order = order500yuan.after(order200yuan).after(orderNormal);
order(1, true, 500); // 输出:500 元定金预购,得到 100 优惠券
order(2, true, 500); // 输出:200 元定金预购,得到 50 优惠券
order(1, false, 500);// 输出:普通购买,无优惠券

最后说一句

如果这篇文章对您有所帮助,或者有所启发的话,帮忙点赞关注一下,您的支持是我坚持写作最大的动力,多谢支持。

同系列文章

  1. JavaScript 设计模式之单例模式
  2. JavaScript 设计模式之策略模式
  3. JavaScript 设计模式之代理模式
  4. JavaScript 设计模式之迭代器模式
  5. JavaScript 设计模式之发布-订阅模式
  6. JavaScript 设计模式之命令模式
  7. JavaScript 设计模式之组合模式
  8. JavaScript 设计模式之模板方法模式
  9. JavaScript 设计模式之享元模式
  10. JavaScript 设计模式之职责链模式
  11. JavaScript 设计模式之中介者模式
  12. JavaScript 设计模式之装饰者模式
  13. JavaScript 设计模式之状态模式
  14. JavaScript 设计模式之适配器模式