设计模式【15】--从审批流中学习责任链模式


已经来到了责任链模式,各位客官听我瞎扯......
责任链模式是什么

责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。(百度百科)
责任链模式是一种行为型设计模式,也就是重点是处理数据,假设我们有一份数据,需要经过很多个节点处理,那么就会是以下这个样子:
设计模式【15】--从审批流中学习责任链模式
文章图片

一个节点处理完之后,交给下一个节点,不知道大家有没有使用过审批流,当我们提完一个审批单后,你的leader审批,leader审批通过之后就是总监批,总监后面可能是高级总监,或者cto,或者hr。他们在同一个链条上,倘若你的leader没有审批完,后面的节点是不可能收到信息的。如果你的leader拒绝了你的申请,那数据也不会到达后面的审批节点。
如果你接触过前端,JS 中点击某个 div 的时候会产生冒泡事件,也就是点击下面的A, AB里面,BC里面, A-> B -> C 会依次收到点击事件:
设计模式【15】--从审批流中学习责任链模式
文章图片

再举个例子,在 SpringMVC中,我们有时候会定义一些拦截器,对请求进行预处理,也就是请求过来的时候,会依次经历拦截器,通过拦截器之后才会进入我们的处理业务逻辑代码。
之前,在做人员管理的时候,有涉及到人员离职情况的处理流程,要交接工作,解除权限,禁用账号等等,这整个处理流程就很适合使用责任链来处理。当然,自动处理流程是会出错的,保存每一个阶段的状态,针对出错的场景,可以手动去从断开责任链的地方接着执行。这整个流程的框架就是应用了责任链,但是根据实际场景也添加了不少其他的东西。
两点疑问
  1. 责任链的每一个节点是不是一定包含下一个节点的引用?
答:不一定,要么把所有责任节点放在一个list里面,依次处理;要么每个节点包含下一个责任节点的引用,
  1. 责任链到底是不允许中断还是不允许中断?
答:两种都可以,不拘泥于细节,可以根据自己的场景使用。
责任链模式中的角色 责任链一般有以下的角色:
  • Client(客户端):调用责任链处理器的处理方法,或者在第一个链对象中调用handle方法。
  • Handler(处理器):抽象类,提供给实际处理器继承然后实现handle方法,处理请求
  • ConcreteHandler(具体处理器):实现handler的类,同时实现handle方法,负责处理业务逻辑类,不同业务模块有不同的ConcreteHandler
  • HandlerChain:负责组合责任链的所有节点以及流程(如果节点包含下一个节点的引用,那么HandlerChain可以不存在)
审批链的实现 下面我们分别来实现不同的写法,假设现在有一个场景,秦怀入职了一家公司,哼哧哼哧干了一年,但是一直没调薪,又过了一年,总得加薪了吧,不加就要提桶跑路了,于是秦怀大胆去内部系统提了一个申请单:【加薪申请】
不中断模式
先演示不中断模式,得先弄个申请单的实体,里面包含了申请单的名字和申请人:
public class Requisition { // 名称 public String name; // 申请人 public String applicant; public Requisition(String name, String applicant) { this.name = name; this.applicant = applicant; } }

责任链中的每个责任节点,也就是处理器,可以抽象成为一个接口:
public interface Handler { // 处理申请单 void process(Requisition requisition); }

我们依次实现了三个不同的责任节点,分别代表leader,总监,hr审批:
public class ManagerHandler implements Handler { @Override public void process(Requisition requisition) { System.out.println(String.format("Manager 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } }

public class DirectorHandler implements Handler{ @Override public void process(Requisition requisition) { System.out.println(String.format("Director 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } }

public class HrHandler implements Handler{ @Override public void process(Requisition requisition) { System.out.println(String.format("Hr 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); } }

责任节点都有了,我们需要用一个责任链把它们组合起来:
public class HandlerChain { List handlers = new ArrayList<>(); public void addHandler(Handler handler){ handlers.add(handler); }public void handle(Requisition requisition){ for(Handler handler:handlers){ handler.process(requisition); } System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name)); } }

客户端测试类:
public class ClientTest { public static void main(String[] args) { HandlerChain handlerChain = new HandlerChain(); handlerChain.addHandler(new ManagerHandler()); handlerChain.addHandler(new DirectorHandler()); handlerChain.addHandler(new HrHandler()); handlerChain.handle(new Requisition("加薪申请","秦怀")); } }

运行结果:
Manager 审批来自[秦怀]的申请单[加薪申请]... Director 审批来自[秦怀]的申请单[加薪申请]... Hr 审批来自[秦怀]的申请单[加薪申请]... 来自[秦怀]的申请单[加薪申请]审批完成

从结果上来看,申请单确实经历过了每一个节点,形成了一条链条,这就是责任链的核心思想。每个节点拿到的都是同一个数据,同一个申请单。
中断模式
秦怀加薪的想法很美好,但是现实很骨感,上面的审批流程一路畅通,但是万一 Hr 想拒绝掉这个申请单了,上面的代码并没有赋予她这种能力,因此,代码得改!(Hr 内心:我就要这个功能,明天上线)。
既然是支持中断,也就是支持任何一个节点审批不通过就直接返回,不会再走到下一个节点,先给抽象的处理节点方法加上返回值:
public interface Handler { // 处理申请单 boolean process(Requisition requisition); }

三个处理节点也同步修改:
public class ManagerHandler implements Handler { @Override public boolean process(Requisition requisition) { System.out.println(String.format("Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } }

public class DirectorHandler implements Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } }

public class HrHandler implements Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return false; } }

处理链调整:
public class HandlerChain { List handlers = new ArrayList<>(); public void addHandler(Handler handler) { handlers.add(handler); }public void handle(Requisition requisition) { for (Handler handler : handlers) { if (!handler.process(requisition)) { System.out.println(String.format("来自[%s]的申请单[%s]审批不通过", requisition.applicant, requisition.name)); return; } } System.out.println(String.format("来自[%s]的申请单[%s]审批完成", requisition.applicant, requisition.name)); } }

修改完成之后的结果:
Manager 审批通过来自[秦怀]的申请单[加薪申请]... Director 审批通过来自[秦怀]的申请单[加薪申请]... Hr 审批不通过来自[秦怀]的申请单[加薪申请]... 来自[秦怀]的申请单[加薪申请]审批不通过

秦怀哭了,加薪的审批被 hr 拒绝了。虽然被拒绝了,但是秦怀也感受到了可以中断的责任链模式,这种写法在处理请求的时候也比较常见,因为我们不希望不合法的请求到正常的处理逻辑中。
包含下一个节点的引用
前面说过,在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。上面的写法都是不包含下一个节点引用的写法。下面我们实践一下,如何使用引用写法完成责任链。
改造Handler接口为抽象类:
public abstract class Handler {private Handler nextHandler; public void setNextHandler(Handler handler) { this.nextHandler = handler; }// 处理申请单 protected abstract boolean process(Requisition requisition); // 暴露方法 public boolean handle(Requisition requisition) { boolean result = process(requisition); if (result) { if (nextHandler != null) { return nextHandler.handle(requisition); } else { return true; } } return false; } }

三个实现类不变:
public class ManagerHandler extends Handler{ @Override boolean process(Requisition requisition) { System.out.println(String.format( "Manager 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } } public class DirectorHandler extends Handler { @Override public boolean process(Requisition requisition) { System.out.println(String.format( "Director 审批通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return true; } }public class HrHandler extends Handler{ @Override public boolean process(Requisition requisition) { System.out.println(String.format("Hr 审批不通过来自[%s]的申请单[%s]...", requisition.applicant, requisition.name)); return false; } }

测试方法,构造嵌套引用:
public class ClientTest { public static void main(String[] args) { HrHandler hrHandler = new HrHandler(); DirectorHandler directorHandler = new DirectorHandler(); directorHandler.setNextHandler(hrHandler); ManagerHandler managerHandler = new ManagerHandler(); managerHandler.setNextHandler(directorHandler); managerHandler.handle(new Requisition("加薪申请","秦怀")); } }

可以看到运行结果也是一样:
Manager 审批通过来自[秦怀]的申请单[加薪申请]... Director 审批通过来自[秦怀]的申请单[加薪申请]... Hr 审批不通过来自[秦怀]的申请单[加薪申请]...

拓展一下
其实责任链配合上Spring更加好用,主要有两点:
1、可以使用注入,自动识别该接口的所有实现类。
@Autowire public List handlers;

2、可以使用@Order注解,让接口实现类按照顺序执行。
@Order(1) public class HrHandler extends Handler{ ... }

源码中的应用
  • Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候执行一些操作。
  • Spring中使用责任链模式来管理Adviser
比如Mybatis中可以添加若干的插件,比如PageHelper,多个插件对对象的包装采用的动态代理来实现,多层代理。
//责任链插件 public class InterceptorChain {private final List interceptors = new ArrayList<>(); // 生成代理对象 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } //一层一层的拦截器 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); }public List getInterceptors() { return Collections.unmodifiableList(interceptors); }}

总结 责任链模式的优点:
  • 降低对象直接的耦合度,对象会自动传递到下一个责任节点,不管是引用方式,还是非引用方式。
  • 增强拓展性,如果需要添加新的责任节点,也比较方便,实现特定的接口即可。
  • 责任节点的顺序可控,可以指定一个顺序属性,排序即可。
  • 每个责任节点职责专一,只处理自己的任务,符合类的单一职责原则。
【设计模式【15】--从审批流中学习责任链模式】责任链的缺点:
  • 如果责任链比较长,性能会受影响。
  • 责任链可能会中途断掉,请求不一定会被接收。
责任链一般是在流程化的处理中,多个节点处理同一份数据,依次传递,可能有顺序要求,也可能没有,处理器的能力抽象成接口,方便拓展。
设计模式系列:
  • 设计模式【1】-- 单例模式到底几种写法?
  • 设计模式【1.1】-- 你想如何破坏单例模式?
  • 设计模式【1.2】-- 枚举式单例有那么好用么?
  • 设计模式【1.3】-- 为什么饿汉式单例是线程安全的?
  • 设计模式【2】-- 简单工厂模式了解一下?
  • 设计模式【2.1】-- 简单工厂模式怎么演变成工厂方法模式?
  • 设计模式【2.2】-- 工厂模式怎么演变成抽象工厂模式?
  • 设计模式【3.1】-- 浅谈代理模式之静态、动态、cglib代理
  • 设计模式【3.2】-- JDK动态代理源码分析有多香?
  • 设计模式【3.3】-- CGLIB动态代理源码解读
  • 设计模式【4】-- 建造者模式详解
  • 设计模式【5】-- 原型模式
  • 设计模式【6.1】-- 初探适配器模式
  • 设计模式【6.2】-- 再聊聊适配器模式
  • 设计模式【7】-- 探索一下桥接模式
  • 设计模式【8】-- 手工耿教我写装饰器模式
  • 设计模式【9】-- 外观模式?没那么高大上
  • 设计模式【10】-- 顺便看看享元模式
  • 设计模式【11】-- 搞定组合模式
  • 设计模式【12】-- 搞定最近大火的策略模式
  • 设计模式【13】-- 模板模式怎么弄?
  • 设计模式【14】-- 从智能音箱中学习命令模式
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,个人网站:http://aphysia.cn,技术之路不在一时,山高水长,纵使缓慢,驰而不息。
剑指Offer全部题解PDF
开源编程笔记

    推荐阅读