责任链模式(职责链模式)

在现实生活中,一个事件需要经过多个对象处理是很常见的场景。例如,采购审批流程、请假流程等。公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据需要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这无疑增加了难度。
在计算机软硬件中也有相关例子,如总线网中数据报传送,每台计算机根据目标地址是否同自己的地址相同来决定是否接收;还有异常处理中,处理程序根据异常的类型决定自己是否处理该异常;


模式的定义与特点

责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
注意:责任链模式也叫职责链模式。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。
责任链模式是一种对象行为型模式,其主要优点如下。

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

其主要缺点如下。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。


应用

假设我们负责一个售卖手机的网站,需求的定义是:经过分别缴纳500元定金和200元定金的两轮预订,现在到了正式购买阶段。公司对于交了定金的用户有一定的优惠政策,规则如下:缴纳500元定金的用户可以收到100元优惠券;纳200元定金的用户可以收到50元优惠券;而没有缴纳定金的用户进入普通购买模式,没有优惠券,而且在库存不足的情况下,不一定能保证买得到。下面开始设计几个字段,解释它们的含义:

  • orderType:表示订单类型,值为1表示500元定金用户,值为2表示200元定金用户,值为3表示普通用户。
  • pay:表示用户是否支付定金,值为布尔值true和false,就算用户下了500元定金的订单,但是如果没有支付定金,那也会降级为普通用户购买模式。
  • stock:表示当前用户普通购买的手机库存数量,已经支付过定金的用户不受限制。

下面把上面的需求用代码实现:

const order = function (orderType, pay, stock) {
  if (orderType === 1) {
    if (pay === true) {
      console.log('500元定金预购,得到100元优惠券')
    } else {
      if (stock > 0) {
        console.log('普通用户购买,无优惠券')
      } else {
        console.log('手机库存不足')
      }
    } else if (orderType === 2) {
      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元优惠券'

虽然通过上面代码我们得到了想要的结果,但是代码难以阅读,维护起来也很困难,如果需要修改需求,那代价无疑是巨大的。
使用职责链模式重构
下面我们使用职责链模式重构,先把500元订单、200元订单以及普通购买拆分成三个函数。代码如下:

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

function order200 (orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200元定金预购,得到50元优惠券')
  } else {
    order200(orderType, pay, stock)
  }
}

function orderNormal (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)   // 手机库存不足

可以看到,重构后的代码已经清晰很多,减少了大量的if-else嵌套,每个函数的职责分明。但是还不够,虽然我们把大函数拆分成了三个小函数,但是请求在链条中传递的顺序很僵硬,传递请求的代码跟业务代码耦合在一起,如果有一天要增加300元定金的预订,那么就要切断之前的链条,修改订单500函数的代码,重新在500和200之间加一根新的链条,这违反了开放-封闭原则。
灵活可拆分的职责链节点
首先修改三个函数,如果某个节点不能处理请求,则返回一个特定的字符串“nextSuccessor”来表示请求需要继续往后传递:

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

function order200 (orderType, pay, stock) {
  if (orderType === 2 && pay === true) {
    console.log('200元定金预购,得到50元优惠券')
  } else {
    return 'nextSuccessor'
  }
}

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

接下来需要定义一个Chain类将三个函数包装进职责链节点:

class Chain {
  construct (fn) {
    this.fn = fn
    this.successor = null
  }

  setNextSuccessor (successor) {
    return this.successor = successor
  }

  passRequest () {
    const res = this.fn.apply(this, arguments)

    if (res === 'nextSuccessor') {
      return this.successor && this.successor.passRequest.apply(this.successor, arguments)
    }
    return res
  }
}

// 包装三个订单函数
const chainOrder500 = new Chain(order500)
const chainOrder200 = new Chain(order200)
const 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预定金的类型,只需要在链中增加一个节点:

function order300 () {
  // 省略代码
}

const chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)

这样的修改简单容易,完全不用理会原来其它订单的代码。

异步的职责链

在上面的例子中,每个节点函数都是同步返回一个特定值来表示是否把请求传递给下一个节点。但是在实际应用中,我们经常会遇到一些异步的问题,比如要在某个节点中通过发起一个ajax异步请求,需要根据异步请求返回的结果才决定是否继续传递请求,这时候我们需要再添加一个函数,手动传递请求给职责链中的下一个节点:

class Chain {
  construct (fn) {
    this.fn = fn
    this.successor = null
  }

  setNextSuccessor (successor) {
    return this.successor = successor
  }

  next () {
    return this.successor && this.successor.passRequest.apply(this.successor, arguments)
  }

  passRequest () {
    const res = this.fn.apply(this, arguments)

    if (res === 'nextSuccessor') {
      return this.successor && this.successor.passRequest.apply(this.successor, arguments)
    }
    return res
  }
}

看一个异步使用的例子:

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

const fn1 = new Chain(function () {
  console.log(2)
  setTimeout(() => {
    this.next()
  }, 1000)
})

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

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

这样我们得到了一个可以处理异步情况的职责链,异步的职责链加上命令模式,可以很方便地创建一个异步ajax队列库。

用AOP实现职责链

前面的例子我们是利用了一个Chain类来把普通函数包装成职责链的节点,利用JavaScript函数式的特性,我们可以实现一种更加方便地方法来创建职责链:

Function.prototype.after = function (fn) {
  const self = this
  return function () {
    const res = self.apply(this, arguments)
    if (res === 'nextSuccessor') {
      return fn.apply(this, arguments)
    }
    return res
  }
}

const order = order500.after(order200).after(orderNormal)
order(1, true, 500)   // 500元定金预购,得到100元优惠券
order(2, true, 500)   // 200元定金预购,得到50元优惠券
order(3, true, 500)   // 普通用户购买,无优惠券
order(1, false, 0)    // 手机库存不足

使用AOP方式实现职责链简单又巧妙,但这种方式把函数叠加在一起,也增加了函数的作用域,如果链条太长,也会有一定的性能问题。


总结

职责链模式的最大优点就是解耦了请求发送者和多个请求接收者之间的关系。其次,使用了职责链模式之后,链中的节点对象可以灵活地拆分重组,增加、删除和修改节点在链中的位置都是很容易地事。它还有一个优点就是,可以手动地指定起始节点,请求并不是一定要从链中的第一个节点开始传递。
当然,这种模式并非没有缺点,首先我们不能保证某个请求一定会被链中的节点处理,所以需要在链尾增加一个保底的接受者处理这种情况。另外职责链模式使得程序中多了一些节点对象,可能在某一次请求传递中,大部分节点并没有起作用,所以过长的职责链会带来性能的损耗。
在JavaScript中。无论是作用链、原型链,还是dom节点中的事件冒泡,我们都能从中找到职责链的影子。


链接: https://fly63.com/course/27_1274