职责链模式

参考

http://c.biancheng.net/view/1383.html

定义

为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

应用场景

可优化含有大量if-else的代码,js中的作用域链、原型链,还是DOM节点中的事件冒泡等都有责职链模式的影子。

  1. 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
  2. 可动态指定一组对象处理请求,或添加新的处理者。
  3. 在不明确指定请求处理者的情况下,向多个处理者中的一个提交请求。

实现、应用思路

职责链模式主要包含以下角色:

  1. 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。

  2. 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。

  3. 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

优点

  1. 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。

  2. 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。

  3. 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。

  4. 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。

  5. 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

缺点

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。

  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。

  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。

代码实例

/* 用责任链模式设计一个请假条审批模块
假如规定学生请假小于或等于 2 天,班主任(ClassAdviser)可以批准;小于或等于 7 天,系主任(DepartmentHead)可以批准;
小于或等于 10 天,院长(Dean)可以批准;其他情况不予批准 */

/* 在日常工作中这很快会想到用if-else来写这一业务代码,代码大概如下。这样会产生以下问题:
1. 随着业务增长leaveApproval会巨大到难以阅读。
2. 修改、维护都会“牵一发而动全身”。
3. 我们可以把相关的具体业务逻辑继续拆分成多个函数来缓解问题1、2,但是可以看到
  任务请求在链条传递中非常僵硬,如果要改变请求顺序,或者增加请求节点,都必须打破
  此链条,同样会有比较高的维护成本。 */

const leaveApproval = days => {
  if (days <= 2) {
    // ClassAdviser
  } else if (days <= 6) {
    // DepartmentHead
  } else if (days <= 10) {
    // Dean
  } else {
    // do not approve
  }
}

/* 用责任链模式重构上面的代码,重构之后我们可以随意增删节点、以及设置起始请求点,
不会影响原有的节点逻辑 */

// 抽象处理者(Handler)角色
abstract class Leader {
  private next: Leader
  setNext(next: Leader) {
    this.next = next
  }
  getNext(): Leader {
    if (this.next) return this.next
    console.log('请假天数太多,没有人批准该假条!')
  }
  abstract handleRequest(days: number): void
}
// 具体处理者班主任类
class ClassAdviser extends Leader {
  constructor() {
    super()
  }
  handleRequest(days) {
    if (days <= 2) {
      console.log(`班主任允许你请${days}天假`)
    } else {
      this.getNext() && this.getNext().handleRequest(days)
    }
  }
}
// 具体处理者系主任类
class DepartmentHead extends Leader {
  handleRequest(days) {
    if (days <= 7) {
      console.log(`系主任允许你请${days}天假`)
    } else {
      this.getNext() && this.getNext().handleRequest(days)
    }
  }
}
// 具体处理者院长类
class Dean extends Leader {
  handleRequest(days) {
    if (days <= 10) {
      console.log(`院长允许你请${days}天假`)
    } else {
      this.getNext() && this.getNext().handleRequest(days)
    }
  }
}
// 客户类(Client)角色
class LeaveApproval {
  // 请求起始对象
  private statr: Leader = null
  // 设置请求链
  constructor() {
    const leader1 = new ClassAdviser()
    const leader2 = new DepartmentHead()
    const leader3 = new Dean()
    leader1.setNext(leader2)
    leader2.setNext(leader3)
    this.setStart(leader1)
  }
  setStart(statr: Leader) {
    this.statr = statr
  }
  // 处理申请
  apply(days: number) {
    this.statr.handleRequest(days)
  }
}

const LA = new LeaveApproval()
LA.apply(1)
LA.apply(3)
LA.apply(9)
LA.apply(11)


/* 在JavaScript中使用AOP实现责职链模式是一种更便捷的方式
什么是面向切面编程AOP?参考:https://www.zhihu.com/question/24863332/answer/48376158
AOP:面向切面编程,这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。
有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
在下面的例子中,我们可以把Function.prototype.next方法看作是切面,而handleRequest1,2,3...等看作是切入点,
next方法动态改变了handleRequest方法的行为。 */

const handleRequest1 = days => {
  if (days <= 2) {
    console.log(`班主任允许你请${days}天假`)
  } else {
    return true
  }
}
const handleRequest2 = days => {
  if (days <= 7) {
    console.log(`系主任允许你请${days}天假`)
  } else {
    return true
  }
}
const handleRequest3 = days => {
  if (days <= 10) {
    console.log(`院长允许你请${days}天假`)
  } else {
    return true
  }
}
const handleRequest4 = () => {
  console.log('请假天数太多,没有人批准该假条!')
}

interface Function {
  next(fun: Function): Function
}
Function.prototype.next = function (fn: Function): Function {
  return (days: number) => {
    const isNext = this(days)
    if (isNext) return fn.call(fn, days)
  }
}

const handleRequest = handleRequest1
  .next(handleRequest2)
  .next(handleRequest3)
  .next(handleRequest4)

handleRequest(1)
handleRequest(4)
handleRequest(8)
handleRequest(20)
上次更新: 11/1/2020, 8:37:15 AM