10_职责链模式

发布时间 2023-05-09 10:56:26作者: pleaseAnswer

1 现实中的职责链模式

中学时代的期末考试,如果你平时不太老实,考试时就会被安排在第一个位置。遇到不会答的题目,就把题目编号写在小纸条上往后传递,坐在后面的同学如果也不会答,他就会把这张小纸条继续递给他后面的人。

  • 优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系

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

我们负责一个售卖手机的电商网站,经过分别缴纳500元定金和200元定金的两轮预定,现在进入了正式购买阶段。

公司对支付过定金的用户有一定的优惠政策

  • 支付过500元定金的用户会收到100元的商城优惠券
  • 支付过200元定金的用户会收到50元的商城优惠券
  • 没有支付过定金的用户进入普通购买模式,并且在库存有限的情况下不能保证买得到

页面加载之初,接口会返回几个字段

image.png

简单实现

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

许多嵌套的条件分支语句 -- 阅读难 维护难

3 用职责链模式重构代码

  1. 先将 500元订单、200元订单、普通购买分成3个函数
  2. 将 orderType pay stock 3字段作为参数传给500元订单函数
  3. 符合处理条件则处理,不符合则传给200元订单函数
  4. 如果200元订单函数依然不能处理请求,则继续传递给普通购买函数

image.png

let oreder500 = function(orderType, pay, stock) {
  if(orderType === 1 && pay === true) {
    console.log('500定金预购,得到100优惠券');
  } else {
    order200(orderType, pay, stock)
  }
}
let order200 = function(orderType, pay, stock) {
  if(orderType === 2 && pay === true) {
    console.log('200定金预购,得到50优惠券');
  } else {
    orederNormal(orderType, pay, stock)
  }
}
let orederNormal = function(orderType, pay, stock) {
  if(stock > 0) {
    console.log('普通购买,无优惠券');
  } else {
    console.log('手机库存不足');
  }
}
  • 代码结构清晰了很多,去掉了许多嵌套的条件分支语句
  • 请求在传递中的顺序非常僵硬,传递请求的代码被耦合在了业务函数中

耦合度高, 违反开放封闭原则

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

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

4.1 改写分别表示3种购买模式的节点函数

约定:如果某个节点不能处理请求,则返回 nextSuccessor 表示请求继续往后传递

let order500 = function(orderType, pay, stock) {
  if(orderType === 1 && pay === true) {
    console.log('500定金预购,得到100优惠券');
  } else {
    return 'nextSuccessor'; // 我不知道下一个节点是谁,反正把请求往后面传递
  }
}
let order200 = function(orderType, pay, stock) {
  if(orderType === 2 && pay === true) {
    console.log('200定金预购,得到50优惠券');
  } else {
    return 'nextSuccessor';// 我不知道下一个节点是谁,反正把请求往后面传递
  }
}
let orderNormal = function(orderType, pay, stock) {
  if(stock > 0) {
    console.log('普通购买,无优惠券');
  } else {
    console.log('手机库存不足');
  }
}

4.2 将函数包装进职责链节点

定义构造函数Chain,在 new Chain 时传递的参数即为需要被包装的函数

class Chain {
  constructor(fn) {
    this.fn = fn;
    this.successor = null; // 表示在链中的下一个节点
  }
  // 指定在链中的下一个节点
  setNextSuccessor(successor) {
    return this.successor = successor;
  }
  // 表示传递请求给某个节点
  passRequest() {
    let ret = this.fn.apply(this, arguments);
    if(ret === 'nextSuccessor') {
      return this.successor 
        && this.successor.passRequest.apply(this.successor, arguments);
    }
    return ret;
  }
}

4.3 将3个订单函数分别包装成职责链的节点

let chainOrder500 = new Chain(order500)
let chainOrder200 = new Chain(order200)
let 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); // 输出:手机库存不足

通过改进,可以自由灵活地增加、移除、修改链中节点顺序

4.4 网站运营人员提出支持300元定金购买

在该链中新增一个节点即可
let order300 = function(orderType, pay, stock) {
    // ...
}
let chainOrder300 = new Chain(order300)
chainOrder500.setNextSuccessor(chainOrder300)
chainOrder300.setNextSuccessor(chainOrder200)

5 异步的职责链

比如在节点函数中发起一个ajax异步请求,请求返回的结构才能决定是否继续在职责链中 passRequest

此时让节点函数同步返回 nextSuccessor 已经没有意义了

  • Chain 类新增 next 表示手动传递请求给职责链中的下一个节点
class Chain {
  // 手动传递请求给职责链中的下一个节点
  next() {
    return this.successor 
      && this.successor.passRequest.apply(this.successor, arguments);
  }
}
应用
let fn1 = new Chain(() => {
  console.log(1);
  return 'nextSuccessor'
})
let fn2 = new Chain(() => {
  console.log(2);
  setTimeout(() => {
    fn2.next()
  }, 1000)
})
let fn3 = new Chain(() => {
  console.log(3);
})
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3)
fn1.passRequest()

6 职责链模式的优缺点

1. 优点

image.png

2. 缺点

  • 不能保证某个请求一定会被链中的节点处理 -> 在链尾增加保底的接受者节点处理即将离开链尾的请求
  • 在一次请求传递中,一些节点对象并没有起到实质性的作用 -> 从性能方面考虑,应避免过长的职责链带来的性能损耗

7 用 AOP 实现职责链

AOP 面向切面编程,通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加某种特定功能的一种技术

约定:如果某个节点不能处理请求,则返回 nextSuccessor 表示请求继续往后传递

Function.prototype.after = function(fn) {
  let _self = this
  return function() {
    let ret = _self.apply(this, arguments)
    if(ret === 'nextSuccessor') {
      return fn.apply(this, arguments)
    }
    return ret
  }
}
let order = order500.after(order200).after(orderNormal);
order(1, true, 500); // 500元定金预购,得到100优惠券
order(2, true, 500); // 200元定金预购,得到5优惠券

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

let getActiveUploadObj = () => {
  try {
    return new ActiveXObject('/.../') // IE控件上传
  } catch(e) {
    return 'nextSuccessor'
  }
}
let getFlashUploadObj = () => {
  if(supportFlash) {
    /.../ // Flash控件上传
  }
  return 'nextSuccessor'
}
let getFormUploadObj = () => {
  return /.../ // Form控件上传
}
let getUploadObj = getActiveUploadObj
  .after(getFlashUploadObj)
  .after(getFormUploadObj)
console.log(getUploadObj());