第十三章 职责链模式

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

我们把这些对象称为链中的节点。

img

13.1 现实中的职责链模式

中学考试时,你遇到不会答的题,就把题目编号写在小纸条上往后传递,坐在后面的同学如果也不会答,他就会把这张纸条传递给他后面的人。这就是职责链。

职责链模式最大的优点是请求发送者只需要知道链中的第一个节点,从而弱化发送者和接收者之间的强联系。

13.2 实际开发中的职责链模式

假设我们负责一个售卖手机的电商网站,用户有三种:缴纳 500 元定金、缴纳 200 元定金、未缴纳定金。

如果缴纳了 500 元定金,那么用户必定能得到一个手机以及 100 元优惠券

如果缴纳了 200 元定金,那么用户必定得到一个手机以及 50 元优惠券

如果未缴纳定金,那么用户购买时不一定能买到,且没有优惠券

我们分别用以下字段来表示:

  • orderType:定金类型(1 为 500 元定金购买者 2 为 200 元定金购买者 3 为普通购买者)
  • pay:用户是否已支付定金,值为 true 或者 false。虽然用户下过 500 元定金,但是一直没付款,那么只能降级为普通购买模式
  • stock:表示当前库存数量,已经支付过定金的用户可以不受限制

我们的代码如下:

js
1var order = function (orderType, pay, stock) {
2  if (orderType === 1) {
3    //500元定金购买模式
4    if (pay) {
5      console.log('500元定金预购,得到100元优惠券')
6    } else {
7      if (stock > 0) {
8        console.log('普通购买,无优惠券')
9      } else {
10        console.log('手机库存不足')
11      }
12    }
13  } else if (orderType === 2) {
14    //200元定金购买模式
15    if (pay) {
16      console.log('200元定金预购,得到50元优惠券')
17    } else {
18      if (stock > 0) {
19        console.log('普通购买,无优惠券')
20      } else {
21        console.log('手机库存不足')
22      }
23    }
24  } else {
25    if (stock > 0) {
26      //普通购买模式
27      console.log('普通购买,无优惠券')
28    } else {
29      console.log('手机库存不足')
30    }
31  }
32}
33order(1, true, 500) //"500元定金预购,得到100元优惠券"

可以看到由于逻辑混乱,上面的代码非常难读。

13.3 用职责链模式重构代码

下面我们重构上面的原始代码,我们首先按照不同的 orderType 分成不同的函数。

js
1var order500 = function (orderType, pay, stock) {
2  if (orderType === 1 && pay) {
3    console.log('500元定金预购,得到100元优惠券')
4  } else {
5    order200(orderType, pay, stock) //请求往下传递 order500和order200耦合了
6  }
7}
8
9var order200 = function (orderType, pay, stock) {
10  if (orderType === 2 && pay) {
11    //200元定金购买模式
12    console.log('200元定金预购,得到50元优惠券')
13  } else {
14    order0(orderType, pay, stock) //请求往下传递 order200和order0耦合了
15  }
16}
17
18var order0 = function (orderType, pay, stock) {
19  if (stock > 0) {
20    console.log('普通购买,无优惠券')
21  } else {
22    console.log('手机库存不足')
23  }
24}
25
26order500(1, true, 500) //"500元定金预购,得到100元优惠券"
27order500(2, true, 500) //"200元定金预购,得到50元优惠券"
28order500(2, false, 500) //"普通购买,无优惠券"
29order500(3, true, 500) //"普通购买,无优惠券"

上面的代码中,order500 会将请求往下传给 order200,order200 会传递给 order0。

现在我们的代码清晰许多了,但是请求在链条传递中的顺序非常僵硬,传递请求也被耦合在业务函数中。

这是违反开放-封闭原则的,如果有一天我们要增加业务逻辑,那么必须改动这些业务函数的内部。

软件工程中,首先应该解决的是重复,其次是维护性。

我们应当让上面的代码在业务发生巨大变动后尽量少地改动。

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

我们可以看到,当 order500 和 order200 都进入 else 判断时,就会往下传递。那我们只需要改写这段代码。

我们在写逻辑时,内部约定只要不成立,就返回 next 表示进入下一个函数。

js
1var order500 = function (orderType, pay, stock) {
2  if (orderType === 1 && pay) {
3    console.log('500元定金预购,得到100元优惠券')
4  } else {
5    return 'next' //不管后面的逻辑,这里就是返回next
6  }
7}
8
9var order200 = function (orderType, pay, stock) {
10  if (orderType === 2 && pay) {
11    //200元定金购买模式
12    console.log('200元定金预购,得到50元优惠券')
13  } else {
14    return 'next'
15  }
16}
17
18var order0 = function (orderType, pay, stock) {
19  if (stock > 0) {
20    console.log('普通购买,无优惠券')
21  } else {
22    console.log('手机库存不足')
23  }
24}

接下来我们需要将函数包装进一些对象中,这些对象中包含当前要处理的函数、下一步要处理什么函数以及处理这些函数的属性。我们的职责链就是通过这些对象来连接的。

js
1{
2  currentExecute:fn,//保存当前步骤的函数
3  nextStep:xxx, // 保存下一步步骤的函数 表示下一个xxx节点
4  start:fn // 开始处理当前步骤和下一步步骤的函数
5}

我们采用 class 构造函数的形式来构造这些对象

js
1class Step {
2  constructor(fn) {
3    this.currentExecute = fn
4    this.nextStep = null
5  }
6  setNextStep(step) {
7    return (this.nextStep = step)
8  }
9  start(...rest) {
10    const result = this.currentExecute.call(this, ...rest)
11    if (result === 'next') {
12      return this.nextStep && this.nextStep.start.call(this.nextStep, ...rest)
13    }
14    return result
15  }
16}

然后使用这个构造函数来构造步骤,并且给每个对象都设置下一步需要做什么的函数

js
1const step1 = new Step(order500)
2const step2 = new Step(order200)
3const step3 = new Step(order0)
4//设置下一步需要做什么
5step1.setNextStep(step2).setNextStep(step3)

最后通过 start 来启动步骤

js
1step1.start(3, false, 0) //"手机库存不足"
2step1.start(3, false, 30) //"普通购买,无优惠券"

上面的代码逻辑已经非常清晰了,我们分别使用 Step 构造函数来构造 step 对象,然后调用 step 对象内部的 setNextStep 来让他们互相连接起来,最后只需要通过第一步的 start 函数就可以运行整个步骤。

通过改进,我们就可以非常灵活地增加、移除、修改链中的顺序。

13.5 异步的职责链

我们让每个节点函数同步返回一个特定的 next,来表示将请求传递给下一个节点。在实际开发中,我们可能会遇到异步的问题,可能需要在节点函数中发起一个 ajax 异步请求,异步请求返回的结果来决定是否要继续在职责链中,这样的话 return 一个特定的字符串就没有意义了。

有没有可能让整个过程更加优雅呢?

比如类似于 koa,通过调用 next 来实现“跳转”的效果。

我们可以封装一个 step.prototype.next 函数,然后交由 currentExecute 中保存的逻辑函数来自行调用。

js
1class Step {
2  constructor(fn) {
3    this.currentExecute = fn
4    this.nextStep = null
5  }
6  setNextStep(step) {
7    return (this.nextStep = step)
8  }
9  //这里是封装的next
10  next(...rest) {
11    return this.nextStep && this.nextStep.start.call(this.nextStep, ...rest)
12  }
13  start(...rest) {
14    const result = this.currentExecute.call(this, ...rest)
15    return result
16  }
17}

逻辑函数则可以通过this.next来自行调用

js
1var order500 = function (orderType, pay, stock) {
2  if (orderType === 1 && pay) {
3    console.log('500元定金预购,得到100元优惠券')
4  } else {
5    this.next(orderType, pay, stock) //通过this来调用
6  }
7}
8
9var order200 = function (orderType, pay, stock) {
10  const self = this
11  if (orderType === 2 && pay) {
12    //200元定金购买模式
13    console.log('200元定金预购,得到50元优惠券')
14  } else {
15    setTimeout(function () {
16      self.next(orderType, pay, stock) //setTimeout中的this变成了window,所以需要改写
17    })
18  }
19}
20
21var order0 = function (orderType, pay, stock) {
22  if (stock > 0) {
23    console.log('普通购买,无优惠券')
24  } else {
25    console.log('手机库存不足')
26  }
27}

现在我们得到了一个特殊的链条,请求在链中的节点里传递,节点有权利决定什么时候把请求给下一个节点。

上面的代码还可以再优化,我们发现 this 的指向需要交由写逻辑函数的人特意指定,

js
1self.next(orderType, pay, stock) //setTimeout中的this变成了window,所以需要改写

如果写逻辑函数的人不知道你对于 Step 类做的细节,那么很有可能就出错了,实际上我们可以将 next 函数通过参数的形式传递给逻辑函数。

js
1class Step {
2  constructor(fn) {
3    this.currentExecute = fn
4    this.nextStep = null
5  }
6  setNextStep(step) {
7    return (this.nextStep = step)
8  }
9  next(...rest) {
10    const next = this.nextStep.next.bind(this.nextStep)
11    return this.nextStep && this.nextStep.start.apply(this.nextStep, [...rest, next]) //这里直接传递给下一个节点的函数
12  }
13  start(...rest) {
14    const next = this.next.bind(this)
15    const result = this.currentExecute.apply(this, [...rest, next]) //这里传递给当前要调用的函数
16    return result
17  }
18}

现在就可以通过参数取出 next 了。

js
1var order500 = function (orderType, pay, stock, next) {
2  if (orderType === 1 && pay) {
3    console.log('500元定金预购,得到100元优惠券')
4  } else {
5    next(orderType, pay, stock)
6  }
7}
8
9var order200 = function (orderType, pay, stock, next) {
10  const self = this
11  if (orderType === 2 && pay) {
12    //200元定金购买模式
13    console.log('200元定金预购,得到50元优惠券')
14  } else {
15    next(orderType, pay, stock)
16  }
17}
18
19var order0 = function (orderType, pay, stock, next) {
20  if (stock > 0) {
21    console.log('普通购买,无优惠券')
22  } else {
23    console.log('手机库存不足')
24  }
25}

类似 koa 的洋葱模型已经完成了。

13.6 职责链模式的优缺点

职责链模式最大的优点就是解耦了请求发送者和 N 个接收者之间的复杂关系,只需要把请求传递给第一个节点即可。

在职责链前我们的逻辑是这样的

img

使用职责链改进后后我们的业务代码逻辑就是这样的

img

使用职责链后,让每种订单都有各自处理的函数而不互相影响。

其次,使用了职责链模式后,链中的节点对象可以灵活拆分重组。增加或者删除一个节点,或者改变节点在链中的位置都可是轻而易举的事情。

职责链模式还有一个优点,我们可以手动指定开始的节点,请求并不是非得从链中的第一个节点开始的。

如果运用得当,职责链模式可以很好地帮助我们组织代码。

职责链模式的缺点在于使得程序中多了一些节点对象,可能在某一次请求传递的过程中,大部分的节点都没有实际的作用,它们的作用仅仅是让请求传递下去,从性能方面考虑,我们要避免过长的职责链带来的性能耗损。

13.7 用 AOP 实现职责链

使用 AOP 的方式比 class 方式更加方便直观,假设我们依然约定每个函数的等式不成立时,返回一个 next

js
1var order500 = function (orderType, pay, stock) {
2  if (orderType === 1 && pay) {
3    console.log('500元定金预购,得到100元优惠券')
4  } else {
5    return 'next'
6  }
7}
8
9var order200 = function (orderType, pay, stock) {
10  if (orderType === 2 && pay) {
11    //200元定金购买模式
12    console.log('200元定金预购,得到50元优惠券')
13  } else {
14    return 'next'
15  }
16}
17
18var order0 = function (orderType, pay, stock) {
19  if (stock > 0) {
20    console.log('普通购买,无优惠券')
21  } else {
22    console.log('手机库存不足')
23  }
24}

然后我们用一个 after 函数来实现切面,当函数返回 next 时,就会进入下一个函数

js
1Function.prototype.after = function (fn) {
2  const self = this
3  return function (...rest) {
4    const result = self.apply(this, rest)
5    if (result === 'next') {
6      return fn.apply(this, rest)
7    }
8    return result
9  }
10}
11
12const order = order500.after(order200).after(order0)
13order(3, false, 500) //"普通购买,无优惠券"
14order(2, true, 500) //"200元定金预购,得到50元优惠券"

用 AOP 来实现职责链既简单又巧妙。

13.9 小结

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

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