前往小程序,Get更优阅读体验!
立即前往
社区首页 >专栏 >设计模式【15】--从审批流中学习责任链模式

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

作者头像
秦怀杂货店
发布于 2022-04-07 12:05:48
发布于 2022-04-07 12:05:48
79200
代码可运行
举报
文章被收录于专栏:技术杂货店技术杂货店
运行总次数:0
代码可运行

Part0前言

剑指Offer & LeetCode刷题仓库https://github.com/Damaer/CodeSolution 文档地址https://damaer.github.io/CodeSolution/ 刷题仓库介绍刷题仓库:CodeSolution 编程知识库https://github.com/Damaer/Coding 文档地址https://damaer.github.io/Coding/#/ 剑指OfferV2 系列已经完成,补增 V2 题目以及C++语言解法,发送 剑指Offer 可获取pdf

设计模式

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

1责任链模式是什么

责任链模式是一种设计模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。(百度百科)

责任链模式是一种行为型设计模式,也就是重点是处理数据,假设我们有一份数据,需要经过很多个节点处理,那么就会是以下这个样子:

一个节点处理完之后,交给下一个节点,不知道大家有没有使用过审批流,当我们提完一个审批单后,你的leader审批,leader审批通过之后就是总监批,总监后面可能是高级总监,或者cto,或者hr。他们在同一个链条上,倘若你的leader没有审批完,后面的节点是不可能收到信息的。如果你的leader拒绝了你的申请,那数据也不会到达后面的审批节点。

如果你接触过前端,JS 中点击某个 div 的时候会产生冒泡事件,也就是点击下面的A, AB里面,BC里面, A-> B -> C 会依次收到点击事件:

再举个例子,在 SpringMVC中,我们有时候会定义一些拦截器,对请求进行预处理,也就是请求过来的时候,会依次经历拦截器,通过拦截器之后才会进入我们的处理业务逻辑代码。

之前,在做人员管理的时候,有涉及到人员离职情况的处理流程,要交接工作,解除权限,禁用账号等等,这整个处理流程就很适合使用责任链来处理。当然,自动处理流程是会出错的,保存每一个阶段的状态,针对出错的场景,可以手动去从断开责任链的地方接着执行。这整个流程的框架就是应用了责任链,但是根据实际场景也添加了不少其他的东西。

2两点疑问

  1. 责任链的每一个节点是不是一定包含下一个节点的引用?

答:不一定,要么把所有责任节点放在一个list里面,依次处理;要么每个节点包含下一个责任节点的引用,

  1. 责任链到底是不允许中断还是不允许中断?

答:两种都可以,不拘泥于细节,可以根据自己的场景使用。

3责任链模式中的角色

责任链一般有以下的角色:

  • Client(客户端):调用责任链处理器的处理方法,或者在第一个链对象中调用handle方法。
  • Handler(处理器):抽象类,提供给实际处理器继承然后实现handle方法,处理请求
  • ConcreteHandler(具体处理器):实现handler的类,同时实现handle方法,负责处理业务逻辑类,不同业务模块有不同的ConcreteHandler
  • HandlerChain:负责组合责任链的所有节点以及流程(如果节点包含下一个节点的引用,那么HandlerChain可以不存在)

4审批链的实现

下面我们分别来实现不同的写法,假设现在有一个场景,秦怀入职了一家公司,哼哧哼哧干了一年,但是一直没调薪,又过了一年,总得加薪了吧,不加就要提桶跑路了,于是秦怀大胆去内部系统提了一个申请单:【加薪申请】

不中断模式

先演示不中断模式,得先弄个申请单的实体,里面包含了申请单的名字和申请人:

代码语言:javascript
代码运行次数:0
复制
public class Requisition {
    // 名称
    public String name;


    // 申请人
    public String applicant;


    public Requisition(String name, String applicant) {
        this.name = name;
        this.applicant = applicant;
    }
}

责任链中的每个责任节点,也就是处理器,可以抽象成为一个接口:

代码语言:javascript
代码运行次数:0
复制
public interface Handler {
    // 处理申请单
    void process(Requisition requisition);
}

我们依次实现了三个不同的责任节点,分别代表leader,总监,hr审批:

代码语言:javascript
代码运行次数:0
复制
public class ManagerHandler implements Handler {
    @Override
    public void process(Requisition requisition) {
        System.out.println(String.format("Manager 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
    }
}
代码语言:javascript
代码运行次数:0
复制
public class DirectorHandler implements Handler{
    @Override
    public void process(Requisition requisition) {
        System.out.println(String.format("Director 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
    }
}
代码语言:javascript
代码运行次数:0
复制
public class HrHandler implements Handler{
    @Override
    public void process(Requisition requisition) {
        System.out.println(String.format("Hr 审批来自[%s]的申请单[%s]...", requisition.applicant, requisition.name));
    }
}

责任节点都有了,我们需要用一个责任链把它们组合起来:

代码语言:javascript
代码运行次数:0
复制
public class HandlerChain {
    List<Handler> 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));
    }
}

客户端测试类:

代码语言:javascript
代码运行次数:0
复制
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("加薪申请","秦怀"));
    }
}

运行结果:

代码语言:javascript
代码运行次数:0
复制
Manager 审批来自[秦怀]的申请单[加薪申请]...
Director 审批来自[秦怀]的申请单[加薪申请]...
Hr 审批来自[秦怀]的申请单[加薪申请]...
来自[秦怀]的申请单[加薪申请]审批完成

从结果上来看,申请单确实经历过了每一个节点,形成了一条链条,这就是责任链的核心思想。每个节点拿到的都是同一个数据,同一个申请单。

中断模式

秦怀加薪的想法很美好,但是现实很骨感,上面的审批流程一路畅通,但是万一 Hr 想拒绝掉这个申请单了,上面的代码并没有赋予她这种能力,因此,代码得改!(Hr 内心:我就要这个功能,明天上线)。

既然是支持中断,也就是支持任何一个节点审批不通过就直接返回,不会再走到下一个节点,先给抽象的处理节点方法加上返回值:

代码语言:javascript
代码运行次数:0
复制
public interface Handler {
    // 处理申请单
    boolean process(Requisition requisition);
}

三个处理节点也同步修改:

代码语言:javascript
代码运行次数:0
复制
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;
    }
}
代码语言:javascript
代码运行次数:0
复制
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;
    }
}
代码语言:javascript
代码运行次数:0
复制
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;
    }
}

处理链调整:

代码语言:javascript
代码运行次数:0
复制
public class HandlerChain {
    List<Handler> 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));
    }
}

修改完成之后的结果:

代码语言:javascript
代码运行次数:0
复制
Manager 审批通过来自[秦怀]的申请单[加薪申请]...
Director 审批通过来自[秦怀]的申请单[加薪申请]...
Hr 审批不通过来自[秦怀]的申请单[加薪申请]...
来自[秦怀]的申请单[加薪申请]审批不通过

秦怀哭了,加薪的审批被 hr 拒绝了。虽然被拒绝了,但是秦怀也感受到了可以中断的责任链模式,这种写法在处理请求的时候也比较常见,因为我们不希望不合法的请求到正常的处理逻辑中。

包含下一个节点的引用

前面说过,**在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。**上面的写法都是不包含下一个节点引用的写法。下面我们实践一下,如何使用引用写法完成责任链。

改造Handler接口为抽象类:

代码语言:javascript
代码运行次数:0
复制
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;
    }
}

三个实现类不变:

代码语言:javascript
代码运行次数:0
复制
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;
    }
}

测试方法,构造嵌套引用:

代码语言:javascript
代码运行次数:0
复制
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("加薪申请","秦怀"));
    }
}

可以看到运行结果也是一样:

代码语言:javascript
代码运行次数:0
复制
Manager 审批通过来自[秦怀]的申请单[加薪申请]...
Director 审批通过来自[秦怀]的申请单[加薪申请]...
Hr 审批不通过来自[秦怀]的申请单[加薪申请]...

拓展一下

其实责任链配合上Spring更加好用,主要有两点:

1、可以使用注入,自动识别该接口的所有实现类。

代码语言:javascript
代码运行次数:0
复制
@Autowire
public List<Handler> handlers;

2、可以使用@Order注解,让接口实现类按照顺序执行。

代码语言:javascript
代码运行次数:0
复制
@Order(1)
public class HrHandler extends Handler{
  ...
}

源码中的应用

  • Mybatis 中的 Plugin 机制使用了责任链模式,配置各种官方或者自定义的 Plugin,与 Filter 类似,可以在执行 Sql 语句的时候执行一些操作。
  • Spring中使用责任链模式来管理Adviser

比如Mybatis中可以添加若干的插件,比如PageHelper,多个插件对对象的包装采用的动态代理来实现,多层代理。

代码语言:javascript
代码运行次数:0
复制
//责任链插件
public class InterceptorChain {

  private final List<Interceptor> 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<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

5总结

责任链模式的优点:

  • 降低对象直接的耦合度,对象会自动传递到下一个责任节点,不管是引用方式,还是非引用方式。
  • 增强拓展性,如果需要添加新的责任节点,也比较方便,实现特定的接口即可。
  • 责任节点的顺序可控,可以指定一个顺序属性,排序即可。
  • 每个责任节点职责专一,只处理自己的任务,符合类的单一职责原则。

责任链的缺点:

  • 如果责任链比较长,性能会受影响。
  • 责任链可能会中途断掉,请求不一定会被接收。

责任链一般是在流程化的处理中,多个节点处理同一份数据,依次传递,可能有顺序要求,也可能没有,处理器的能力抽象成接口,方便拓展。

设计模式系列

【作者简介】

秦怀,作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。个人网站:http://aphysia.cn

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-02-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 秦怀杂货店 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【leetcode刷题】T131-二叉搜索树的最近公共祖先
https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
木又AI帮
2019/08/02
4160
golang刷leetcode 二叉树(10)最近公共祖先
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
golangLeetcode
2022/08/02
2180
golang刷leetcode 二叉树(10)最近公共祖先
二叉搜索树的公共祖先问题!
题目链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
代码随想录
2021/09/08
3520
二叉搜索树的公共祖先问题!
二叉搜索树的最近公共祖先
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
木子星兮
2020/07/17
4360
leetcode刷题(45)——35. 二叉搜索树的最近公共祖先
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
老马的编程之旅
2022/06/23
1570
leetcode刷题(45)——35. 二叉搜索树的最近公共祖先
图解LeetCode——剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
根据题目描述,我们给我们两个节点TreeNode p和TreeNode q,然后在二叉搜索树中去寻找最近公共祖先。那么题目中给出了非常关键的一个信息就是——二叉搜索树,那么这种二叉树具有如下的特征:
爪哇缪斯
2023/05/10
1620
图解LeetCode——剑指 Offer 68 - I. 二叉搜索树的最近公共祖先
二叉树的最近公共祖先
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
初阶牛
2023/10/14
2230
二叉树的最近公共祖先
腾讯精选50题算法【二叉搜索树的最近公共祖先】
最近几周掺杂着需求、以及一些琐碎的事情,算法的学习一直都是默默的在搞,没有形成文章。
程序员小跃
2019/12/25
7360
​LeetCode刷题实战235:二叉搜索树的最近公共祖先
算法的重要性,我就不多说了吧,想去大厂,就必须要经过基础知识和业务逻辑面试+算法面试。所以,为了提高大家的算法能力,这个公众号后续每天带大家做一道算法题,题目就从LeetCode上面选 !
程序员小猿
2021/04/15
2940
【剑指Offer】68.1 二叉搜索树的最近公共祖先
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
瑞新
2020/12/07
2640
【剑指Offer】68.1 二叉搜索树的最近公共祖先
二叉树的最近公共祖先
这道题目的看代码比较简单,而且好像也挺好理解的,但是如果把每一个细节理解到位,还是不容易的。
代码随想录
2021/09/08
2.6K0
二叉树的最近公共祖先
二叉树子节点的最近父节点
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
conanma
2021/06/08
1.8K0
【二叉树进阶】二叉树经典面试题——最近公共祖先问题
7和4呢,2 、5 、3是不是都是它们两个的公共祖先啊,但是题目要求找最近的公共祖先,所以是2。 再看一种情况
YIN_尹
2024/01/23
1420
【二叉树进阶】二叉树经典面试题——最近公共祖先问题
【一天一大 lee】二叉搜索树的最近公共祖先 (难度:简单) - Day2020092
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
前端小书童
2020/09/30
3270
【一天一大 lee】二叉搜索树的最近公共祖先 (难度:简单) - Day2020092
[第33期] 树,二叉树, 二叉搜索树
比如想想访问中间某个结点的时候,或者倒数第几个结点 就只能从头往后一个一个查, 效率不高。
皮小蛋
2020/02/29
5260
一文秒杀 5 道最近公共祖先问题
读完本文,可以去力扣解决如下题目: 236. 二叉树的最近公共祖先(中等) 1644. 二叉树的最近公共祖先 II(中等) 1650. 二叉树的最近公共祖先 III(中等) 1676. 二叉树的最近公共祖先 IV(中等) 235. 二叉搜索树的最近公共祖先(简单) 如果说笔试的时候经常遇到各种动归回溯的骚操作,那么面试会倾向于一些比较经典的问题,难度不算大,而且也比较实用。 本文就用 Git 引出一个经典的算法问题:最近公共祖先(Lowest Common Ancestor,简称 LCA)。 git pull 这个命令我们经常会用,它默认是使用 merge 方式将远端别人的修改拉到本地;如果带上参数 git pull -r,就会使用 rebase 的方式将远端修改拉到本地。 这二者最直观的区别就是:merge 方式合并的分支会看到很多「分叉」,而 rebase 方式合并的分支就是一条直线。但无论哪种方式,如果存在冲突,Git 都会检测出来并让你手动解决冲突。 那么问题来了,Git 是如何合并两条分支并检测冲突的呢? 以 rebase 命令为例,比如下图的情况,我站在 dev 分支执行 git rebase master,然后 dev 就会接到 master 分支之上:
labuladong
2022/03/31
1.7K1
一文秒杀 5 道最近公共祖先问题
LeetCode 236:二叉树的最近公共祖先
输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出:5 解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。
Wu_Candy
2022/07/04
3070
LeetCode 236:二叉树的最近公共祖先
【leetcode刷题】T132-二叉树的最近公共祖先
https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/
木又AI帮
2019/08/06
3610
二叉树OJ题(C++实现)
主要思路是借助一个队列,将每一层的数据以size统计,当size为0时说明该层数据已经输入完,将这一层的数据传入vector中,再通过push_back 传入 vector< vector< int >中
lovevivi
2023/10/16
1870
二叉树OJ题(C++实现)
LeetCode 二叉树的最近公共祖先(二叉树)
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
SakuraTears
2022/01/13
2000
LeetCode 二叉树的最近公共祖先(二叉树)
推荐阅读
相关推荐
【leetcode刷题】T131-二叉搜索树的最近公共祖先
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
查看详情【社区公告】 技术创作特训营有奖征文