在软件开发中,我们经常会遇到这样的场景:一个请求需要经过多个对象处理,但发送者并不关心具体由谁处理,只关心请求最终能被处理。职责链模式(Chain of Responsibility Pattern)正是为此而生——它使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。

本文将从现实生活中的例子入手,通过订单处理的重构案例,深入讲解职责链模式的核心思想,并展示其灵活的实现方式,包括异步职责链和AOP实现。

一、现实中的职责链模式

在现实生活中,职责链模式的例子比比皆是:

  • 公交车投币早高峰公交车上人多拥挤,上车后找不到售票员,只好把两块钱硬币往前递。除非站在你前面的第一个人就是售票员,否则你的硬币通常要在N个人手上传递,才能最终到达售票员手里。

  • 考试传纸条中学期末考试,遇到不会答的题目,就把题目编号写在小纸条上往后传递。坐在后面的同学如果也不会答,他就会把这张小纸条继续递给他后面的人。

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

二、实际开发中的职责链模式

2.1 问题场景:手机订单处理

假设我们负责一个售卖手机的电商网站,经过分别交纳500元定金和200元定金的两轮预定后,现在进入正式购买阶段。优惠政策如下:

  • 支付500元定金的用户:获得100元优惠券
  • 支付200元定金的用户:获得50元优惠券
  • 未支付定金的用户:普通购买,无优惠券,且受库存限制

页面加载时,后端会传递以下几个字段:

  • orderType:1=500元定金用户,2=200元定金用户,3=普通购买用户
  • pay:是否已支付定金
  • stock:普通购买的手机库存数量

最初的代码实现如下:

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) { // 200元定金购买模式
    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优惠券

这段代码存在明显缺点:

  1. 函数巨大,难以阅读
  2. 违反开放-封闭原则,每次新增或修改订单类型都需要改动函数内部
  3. 状态切换不明显,维护困难

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

3.1 初步重构:拆分成独立函数

我们将500元订单、200元订单、普通购买分别拆分成三个函数,通过函数调用传递请求:

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

// 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元定金预购,得到50优惠券
order500(3, false, 500);  // 输出:普通购买,无优惠券
order500(3, false, 0);    // 输出:手机库存不足

虽然代码结构清晰了,但请求传递顺序被硬编码在函数内部,仍然违反开放-封闭原则。

3.2 灵活可拆分的职责链节点

我们定义一个Chain类,用于包装函数,并支持链式设置后继节点:

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;
};

然后改写订单函数,使其返回'nextSuccessor'表示无法处理:

var order500 = function (orderType, pay, stock) {
  if (orderType === 1 && pay === true) {
    console.log('500元定金预购,得到100优惠券');
  } else {
    return 'nextSuccessor';
  }
};

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('手机库存不足');
  }
};

最后包装成节点并设置链条顺序:

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);

四、异步的职责链

在实际开发中,我们经常会遇到异步操作,比如在节点中发起ajax请求,根据返回结果决定是否继续传递请求。此时,我们需要让节点可以手动触发下一个节点。

Chain增加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 → 2(1秒后) → 3

这种异步职责链配合命令模式,可以方便地创建异步ajax队列库。

五、职责链模式的优缺点

5.1 优点

  • 解耦:请求发送者和接收者之间的复杂关系被解耦,发送者只需将请求传递给第一个节点即可。
  • 灵活重组:链中的节点对象可以灵活拆分、重组,增加或删除节点很容易,且符合开放-封闭原则。
  • 可指定起始节点:请求不一定从链的第一个节点开始,可以根据需求手动指定,减少传递次数。

5.2 缺点

  • 无法保证处理:不能保证某个请求一定会被链中的节点处理,需要在链尾增加保底节点。
  • 性能损耗:大部分节点可能仅起到传递作用,过长的链会影响性能。

六、用AOP实现职责链

利用JavaScript的函数式特性,我们可以通过Function.prototype.after更加简洁地实现职责链:

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 = order500.after(order200).after(orderNormal);

order(1, true, 500);   // 输出:500元定金预购,得到100优惠券
order(2, true, 500);   // 输出:200元定金预购,得到50优惠券
order(1, false, 500);  // 输出:普通购买,无优惠券

用AOP实现职责链既简单又巧妙,但链条过长时,函数作用域的叠加也会影响性能。

七、用职责链模式获取文件上传对象

回顾迭代器模式中获取文件上传对象的例子,我们可以用职责链模式更简单地实现:

var getActiveUploadObj = function () {
  try {
    return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE上传控件
  } catch (e) {
    return 'nextSuccessor';
  }
};

var getFlashUploadObj = function () {
  if (supportFlash()) {
    var str = '<object type="application/x-shockwave-flash"></object>';
    return $(str).appendTo($('body'));
  }
  return 'nextSuccessor';
};

var getFormUpladObj = function () {
  return $('<form><input name="file" type="file"/></form>').appendTo($('body'));
};

var getUploadObj = getActiveUploadObj.after(getFlashUploadObj).after(getFormUpladObj);
console.log(getUploadObj());

八、小结

职责链模式是容易被忽视但非常实用的模式。它通过将请求沿着链传递,直到有对象处理,实现了请求发送者与接收者的解耦。在JavaScript中,我们可以通过面向对象的方式或AOP的方式实现职责链。

无论是作用域链、原型链,还是DOM节点中的事件冒泡,都能找到职责链模式的影子。学会使用职责链模式,将帮助我们更好地组织代码,提高系统的灵活性和可维护性。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐