从零开始学习Java设计模式 | 行为型模式篇:责任链模式

Posted 李阿昀

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从零开始学习Java设计模式 | 行为型模式篇:责任链模式相关的知识,希望对你有一定的参考价值。

在本讲,我们来学习一下行为型模式里面的第四个设计模式,即责任链模式。

概述

在学习责任链模式之前,我们先来看一下下面这段描述。

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但是每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了员工请假的难度。因为领导有很多,员工到底找哪位领导他还得自己判断,所以这会显得特别特别麻烦。这样的例子还有很多,如找领导出差报销、生活中的"击鼓传花"游戏等。

说了这么多,不知你有没有在公司请过假,要是你请过假,想想是不是这么一回事啊!很显然,在该例子中,请假就是一个请求,而且多个对象都可以处理该请求,有部门负责人、副总经理、总经理等,他们都可以进行批假,但是每个对象的处理条件或权限不同,比如部门负责人有可能只能批1~2天的假,一旦超过这一请假天数,员工就得去找部门负责人的顶头上司,也就是副总经理了,要是还超过了副总经理批假的一个范围的话,那么员工就得再去找总经理批假了,这是不是就增加了员工请假的难度啊!

既然问题出现了,那么又该如何去解决呢?使用责任链模式。那什么又是责任链模式呢?下面我们就来看一看它的概念。

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

很多人看完,完全不知道啥意思,这里我就为大家稍微解释解释。就以员工请假案例来说,请求发送者指的就是员工,因为是员工(例如张三)要请假的;多个请求处理者指的是部门负责人、副总经理、总经理等这些人。这样,张三请假的示意图就是下面这样了。

从上图中可以看到,张三要请假的话,那么他只需要去找自己部门的负责人就可以了,因为对于他来说,他肯定知道自己部门的负责人是谁。然后,部门负责人会根据张三请假的天数来决定是否批假,如果部门负责人能批假,那么自然就帮张三批了;可如果他不能批,那么他就会去找他的顶头上司,即副总经理,因为他们已经连成一条链了。同理,副总经理也是一样,他也会根据他所能批准的请假天数来判断,如果在自己的批准范围之内,那么废话不多说,直接批假;如果批不了的话,那么再去找对应他的顶头上司,即总经理。于此一来,当整个链走完,张三请假的流程就算是结束了。

理解了责任链模式的概念之后,接下来,我们再来看一下责任链模式的结构。

结构

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

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接(即记住下一个对象的引用)。

    注意了,对于该角色,我们既可以定义成接口,也可以定义成抽象类,一般来说,我们都会定义成抽象类。

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

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

    也就是说,客户类不需要去找对应的对象进行处理,而只需将处理链创建好即可。就拿上述张三请假的示意图来说,他只需要找他自己的部门负责人即可,至于请假流程要经过哪几步,他并不需要去关注。

责任链模式案例

接下来,我们通过一个案例来让大家更好地去理解一下责任链模式。

分析

现需要开发一个请假流程控制系统。请一天以下的假只需要小组长同意即可;请1天到3天的假还需要部门经理同意;请3天到7天的假还需要总经理同意才行。

很明显,要想解决该需求,我们就得使用责任链模式。下面是我为该请假流程控制系统设计出的类图,大家可要好好看看哟!

通过以上类图,大家可以看到要设计的请假流程控制系统究竟都涉及到了哪些类,以及类和类之间的关系。下面,我就为大家稍微讲解一下以上类图。

可以看到,上图左侧有一个请假条类,即LeaveRequest,它里边包含有name、num和content这仨属性,它们分别表示请假人的名称、请假的天数以及请假的原因。而且,在该类里面,我们还提供了一个构造方法,在构造方法里面你需要传入三个参数分别为name、num和content这仨属性赋值,当然了,这仨属性肯定还应有各自对应的getter方法,不过大家在这里要注意,我们并没有为这仨属性提供对应的setter方法,这是因为我们已然通过构造方法为这仨属性赋值了,而不再需要通过setter方法来进行赋值了,当然了,你也可以提供,不过这个得根据你具体的需求来定了。

在上图右侧,我们能看到一个叫Handler的类,该类就充当着责任链模式里面的抽象处理者角色,它里面定义了三个常量,分别是NUM_ONE(值为1)、NUM_THREE(值为3)、NUM_SEVEN(值为7),很显然这三个常量的值就是请假天数的临界点。注意,它们都是protected来修饰的,这样,Handler类的子类就可以直接去使用它们了。

为啥要在Handler类里面定义三个常量呢?看一下最开始的需求,你就知道了。如果员工只请1天以下的假,那么小组长同意就可以了;如果员工请1-3天的假,那么部门经理同意就可以了;如果员工请3-7天的假,那么总经理同意就可以了,所以在Handler类里面我们就要定义NUM_ONE(值为1)、NUM_THREE(值为3)、NUM_SEVEN(值为7)这三个常量了,这样,我们用起来也会方便一些。

我们还是继续来看一下Handler类,可以看到它里面还定义了numStart和numEnd两个成员变量,它俩分别表示请假的开始时间和结束时间。啥意思呢?以员工请假1天以下来说,很显然,他就得找小组长来批假了,在小组长看来,请假的开始时间就是0天,而请假的结束时间则是1天。不知我这样解释,大家明白了没有?

此外,Handler类里面还定义有一个成员变量,那就是nextHandler,且还是Handler类型的,也即后继者,所以从上图中我们可以看到该类是自己聚合了自己。

其实,在讲责任链模式的概念时,我就讲过,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一个对象记住其下一个对象的引用而连成一条链。对此,在该案例里面是这样体现出来的:小组长的后继者是部门经理,而部门经理的后继者就是总经理。所以,大家明白了没?

当然了,Handler类里面还提供了两个构造方法,除此之外,它里面还有三个方法,一个是setNextHandler,用于设置后继者;一个是submit,用于提交请假的请求;还有一个是handleLeave,用于处理请假的请求。

至此,对于这个Handler类,我们就算是分析完了。接下来,我们就来看看Handler类的一些子类。

Handler类的子类在这里我设计出来了三个,一个是GroupLeader,即小组长;一个是Manager,即部门经理;还有一个是GeneralManager,即总经理。由于在父类(即Handler类)中定义了一个抽象的方法,即handleLeave,所以这仨子类就必须得去重写该方法了。当然了,它们都还分别定义有各自对应的构造方法。

以上就是我们对类图的分析,类图中所涉及到的类,以及类和类之间的关系,相信大家也已经搞清楚了。当然了,以上类图中还有一个客户端类,该类非常简单,不值得我们去关注它,所以我就没分析它了。

实现

上面我们已经分析过了案例里面所涉及到的类,以及类和类之间的关系,接下来我们就要编写代码来实现该案例了。

首先,打开咱们的maven工程,并在com.meimeixia.pattern包下新建一个子包,即responsibility,也即责任链模式的具体代码我们是放在了该包下。

然后,创建请假条类,这里我们就起名为了LeaveRequest。

package com.meimeixia.pattern.responsibility;

/**
 * 请假条类
 * @author liayun
 * @create 2021-09-16 17:41
 */
public class LeaveRequest {

    // 请假人姓名
    private String name;
    // 请假天数
    private int num;
    // 请假内容
    private String content;

    public LeaveRequest(String name, int num, String content) {
        this.name = name;
        this.num = num;
        this.content = content;
    }

    public String getName() {
        return name;
    }

    public int getNum() {
        return num;
    }

    public String getContent() {
        return content;
    }

}

接着,创建抽象处理者类,这个类我们就命名为Handler。

package com.meimeixia.pattern.responsibility;

/**
 * 抽象处理者类
 * @author liayun
 * @create 2021-09-16 17:56
 */
public abstract class Handler {

    // 定义三个常量
    protected final static int NUM_ONE = 1;
    protected final static int NUM_THREE = 3;
    protected final static int NUM_SEVEN = 7;

    // 该领导处理的请假天数区间
    private int numStart; // 请假的开始时间。例如,对部门经理而言,他的numStart就是1
    private int numEnd; // 请假的结束时间。例如,对部门经理而言,他的numEnd就是3

    // 声明后继者(即声明上级领导)
    private Handler nextHandler;

    public Handler(int numStart) {
        this.numStart = numStart;
    }

    public Handler(int numStart, int numEnd) {
        this.numStart = numStart;
        this.numEnd = numEnd;
    }

    // 设置后继者(即设置上级领导)
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    // 各级领导处理请假条的方法。注意,该方法是一个抽象方法,因为不同的领导处理请假条可能稍微有点不一样
    protected abstract void handleLeave(LeaveRequest leave);

    /**
     * 提交请假条。例如,张三要请假,那么他得进行一个提交,即将请假条提交给他的小组长,若小组长能处理则处理,
     *           若处理不了,则他就要把张三的请假条再提交给他的上级领导了,以此类推...
     *
     * 注意了,该方法我们要声明成final的,这是因为要求子类不能去重写该方法。
     */
    public final void submit(LeaveRequest leave) {
        // 该领导进行审批
        this.handleLeave(leave);
        // 该领导审批完了之后,还得进行一个判断,判断他还有没有上级领导,以及请假天数是否超出他最大处理的请假天数
        if (this.nextHandler != null && leave.getNum() > this.numEnd) {
            // 若还有上级并且请假天数超过了当前领导的处理范围,则提交给上级领导进行审批
            this.nextHandler.submit(leave);
        } else {
            /*
             * 请假流程结束有两个条件:
             *      1. 当前领导没有上级领导了,也就是说当前领导就是最大的领导
             *      2. 请假天数在当前领导审批的范围之内
             *
             *      例如,张三要请两天的假,由于小组长只能处理1天以下的假,所以他就会把请假条继续提交给部门经理进行审批,
             *      对于部门经理而言,张三请假的天数在他审批的范围之内,这样,部门经理直接审批就完事了,也就是说请假流程
             *      到部门经理这块就结束了,而不需要继续再往总经理那边走了。
             */
            System.out.println("流程结束!");
        }
    }

}

抽象处理者类创建完毕之后,接下来我们就要开始创建它的一些子类了。

这里,我们先创建第一个子类,即小组长类,该类我们就命名为GroupLeader了。

package com.meimeixia.pattern.responsibility;

/**
 * 小组长类(具体的处理者)
 * @author liayun
 * @create 2021-09-16 21:10
 */
public class GroupLeader extends Handler {

    public GroupLeader() {
        // 小组长能处理1天以下的请假
        super(0, Handler.NUM_ONE);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
        System.out.println("小组长审批:同意");
    }

}

再创建第二个子类,即部门经理类,该类我们就命名为Manager了。

package com.meimeixia.pattern.responsibility;

/**
 * 部门经理类(具体的处理者)
 * @author liayun
 * @create 2021-09-16 21:10
 */
public class Manager extends Handler {

    public Manager() {
        // 部门经理能处理1-3天的请假
        super(Handler.NUM_ONE, Handler.NUM_THREE);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
        System.out.println("部门经理审批:同意");
    }

}

紧着再创建最后一个子类,即总经理类,该类我们就命名为GeneralManager了。

package com.meimeixia.pattern.responsibility;

/**
 * 总经理类(具体的处理者)
 * @author liayun
 * @create 2021-09-16 21:10
 */
public class GeneralManager extends Handler {

    public GeneralManager() {
        // 总经理能处理3-7天的请假
        super(Handler.NUM_THREE, Handler.NUM_SEVEN);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
        System.out.println("总经理审批:同意");
    }

}

至此,具体的处理者类我们就已经全部创建完毕了。可能有些同学会说,如果员工的请假天数超出了已知的最大处理范围,那么又该怎么办呢?很简单嘛,直接不予批准就可以了,所以大家就不要去提交这种无效的请假条了。

最后,我们来创建一个客户端类用于测试。

package com.meimeixia.pattern.responsibility;

/**
 * @author liayun
 * @create 2021-09-16 22:10
 */
public class Client {
    public static void main(String[] args) {
        // 创建一个请假条对象
        LeaveRequest leave = new LeaveRequest("小明", 4, "身体不适");

        // 创建各级领导对象
        GroupLeader groupLeader = new GroupLeader();
        Manager manager = new Manager();
        GeneralManager generalManager = new GeneralManager();

        // 设置处理者链,即每一个领导记住他的上一级领导
        groupLeader.setNextHandler(manager);
        manager.setNextHandler(generalManager);

        // 小明提交请假申请
        groupLeader.submit(leave);
    }
}

此时,运行以上客户端类,打印结果如下图所示,可以看到小明的请假天数是4天,那么就得经过小组长审批→部门经理审批→总经理审批这样一个请假流程,走完全程最终交给总经理来审批。

责任链模式的优缺点

接下来,我们来看一看责任链模式的优缺点。

优点

责任链模式的优点还是比较多的,我总结出了如下几点。

  1. 降低了对象之间的耦合度。

    这里,我们要明确责任链模式究竟降低了哪些对象之间的耦合度。我这里直接就给大家说了,责任链模式降低了请求发送者和请求接收者这俩之间的耦合度。

  2. 增强了系统的可扩展性。

    可以根据需要增加新的请求处理类,例如,在上述案例中,如果后期员工请假还得经过董事长,那么我们只需要再去定义一个董事长类,然后在链中把董事长类的对象添加进来就可以了,这也满足了开闭原则。

  3. 增强了给对象指派职责的灵活性。

    当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

    例如,在上述案例中,如果后期员工请假还得经过董事长,那么我们只需要再去定义一个董事长类,然后在链中把董事长类的对象添加进来就可以了。如果后期员工请假不需要经过总经理了,那么我们只需动态地删除链内的总经理对象即可。这样,是不是就具有灵活性了啊!

  4. 责任链简化了对象之间的连接。

    一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这样就能避免使用众多的if或者if else语句了。

    看完上面这句话之后,有些人可能会说,这不对啊!在咱们上述案例的代码里面,不是也用到了if else语句了嘛,Handler类里面的submit方法就用到了啊!注意,这里大家一定要记住,这块所说的避免了使用众多的if或者if else语句,是针对客户端来说的。

  5. 责任分担。

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

缺点

关于责任链模式的缺点,我总结出了如下几点。

  1. 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。例如,小明要是请假7天以上的话,那么他会发现没有任何领导可以处理他的请求。
  2. 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。所以,职责链不易过长,适当就好。
  3. 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用,就是说有可能我们会将职责链设置成了一个环形,这样运行的时候就会出问题,即造成循环调用。

责任链模式在JavaWeb源码中的应用

接下来,我们来看一下责任链模式在我们学过的JavaWeb应用开发中的具体应用。

在JavaWeb应用开发中,FilterChain就是责任链(过滤器)模式的典型应用。下面,我们就来简单模拟一下FilterChain。注意,这里我就不再像之前那样把相关的类以及接口的源码拿出来分析了,因为源码的实现还是很复杂的,所以这里我只是简单地去模拟了一下FilterChain而已,目的主要是看一下FilterChain底层所用到的责任链模式。

首先,创建两个接口,一个是Request,它是模拟web请求的Request接口。

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 22:59
 */
public interface Request {
    
}

一个是Response,它是模拟web响应的Response接口。

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 22:59
 */
public interface Response {
    
}

注意,在以上两个接口中我们并没有提供任何方法,因为意义不大,这俩接口创建出来也只是为了补全语法而已。

然后,再创建一个模拟web过滤器的Filter接口,它里面定义有一个doFilter方法,而且该方法需要传递三个参数,一个是Request,一个是Response,还有一个是FilterChain,至于FilterChain,你等会就能看到了。

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 23:00
 */
public interface Filter {
    public void doFilter(Request req, Response res, FilterChain c);
}

接着,创建以上Filter接口的子实现类,也即模拟具体过滤器。这里,我们创建了两个子实现类,一个是FirstFilter,具体实现代码如下:

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 23:15
 */
public class FirstFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {
        System.out.println("过滤器1 前置处理");

        // 先执行所有request再倒序执行所有response
        chain.doFilter(request, response);

        System.out.println("过滤器1 后置处理");
    }
}

一个是SecondFilter,具体实现代码如下:

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 23:16
 */
public class SecondFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {
        System.out.println("过滤器2 前置处理");

        // 先执行所有request再倒序执行所有response
        chain.doFilter(request, response);

        System.out.println("过滤器2 后置处理");
    }
}

以上两个过滤器创建完毕之后,接下来我们来创建FilterChain类,也即模拟过滤器链。

package com.meimeixia.pattern.responsibility.jdk;

import java.util.ArrayList;
import java.util.List;

/**
 * @author liayun
 * @create 2021-09-25 23:07
 */
public class FilterChain {
    private List<Filter> filters = new ArrayList<Filter>();

    private int index = 0;

    /**
     * 添加过滤器
     *
     * 调用该方法,其实就是把过滤器添加到以上List集合里面。而且,该方法对外还有一个作用,创建过滤器链对象,即将过滤器链对象组建好。
     * @param filter
     * @return
     */
    public FilterChain addFilter(Filter filter) {
        this.filters.add(filter);
        return this;
    }

    public void doFilter(Request request, Response response) {
        if (index == filters.size()) {
            return;
        }
        Filter filter = filters.get(index);
        index++;
        filter.doFilter(request, response, this);
    }
}

注意,这儿我只是简单地模拟了一下而已,要是大家感兴趣的话,不妨去看一下FilterChain类的源码,你大概就能知道它的底层实现了。

最后,创建一个测试类用于测试。

package com.meimeixia.pattern.responsibility.jdk;

/**
 * @author liayun
 * @create 2021-09-25 23:18
 */
public class Client {
    public static void main(String[] args) {
        /*
         * 这里我们声明了两个变量,一个是Request类型的req,还有一个是Response类型的res,
         * 而且它俩都被赋予了一个null的值,这是为了补全语法,不至于让程序在编译以及运行时报错!
         */
        Request req = null;
        Response res = null;

        FilterChain filterChain = new FilterChain();
        // 组建过滤器链对象
        filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter());
        filterChain.doFilter(req, res);
    }
}

此时,运行以上测试类,打印结果如下图所示,可以看到这个结果和我们真正的去使用JavaWeb中的过滤器以及过滤器链时的结果是一样的。

接下来,我们就来分析一下打印结果为什么会是上面这样。

在测试类中可以看到,我们组建好过滤器链对象之后,旋即调用了FilterChain对象的doFilter方法,所以我们不妨进入到FilterChain类的doFilter方法中去看看。

从上可知,doFilter方法里面首先会做一个判断,当然,此时是并不满足if判断条件的,因为现在List集合里面有两个元素,而index却是等于0,所以此时程序并不会进入到if判断语句中,而是向下执行。

程序向下执行时,可以看到会从List集合里面去获取0索引位置的过滤器对象,即FirstFilter类的对象,获取到该对象之后,index会先进行一个加加,然后再去调用该对象(即FirstFilter类的对象)的doFilter方法。

于是,我们进入到FirstFilter类的doFilter方法里面去看一看,看一下该方法是如何实现的。

可以看到,doFilter方法先是打印了一句话,即过滤器1 前置处理,然后再调用了FilterChain对象里面的doFilter方法。注意,此时,后面的过滤器1 后置处理这句话还没打印呢,至于什么时候打印,我待会再来说。

很显然,我们又得回到FilterChain类里面的doFilter方法中来分析。由于刚才index++了,所以现在index的值已经变成1了,但是这依然满足不了if判断条件,故而程序还是不会进入if判断语句里面,而是向下执行。

程序向下执行时,可以看到又会从List集合里面去获取1索引位置的过滤器对象,即SecondFilter类的对象,获取到该对象之后,index会先进行一个加加,然后再去调用该对象(即SecondFilter类的对象)的doFilter方法。

于是,我们进入到SecondFilter类的doFilter方法里面去看一看,看一下该方法是如何实现的。

可以看到,doFilter方法先是打印了一句话,即过滤器2 前置处理,然后再调用了FilterChain对象里面的doFilter方法。注意,此时,后面的过滤器2 后置处理这句话还没打印呢,至于什么时候打印,我待会再来说。

很显然,我们又得再次回到FilterChain类里面的doFilter方法中来分析了。由于刚才index++了,所以现在index的值已经变成2了,此时恰好满足了if判断条件,故而程序会进入if判断语句中,直接返回。

返回到哪去呢?返回到SecondFilter类的doFilter方法中,由于FilterChain对象里面的doFilter方法已经调用完了,所以此时会打印后面的过滤器2 后置处理这句话。

打印完之后,接着又会返回到FirstFilter类的doFilter方法中,由于FilterChain对象里面的doFilter方法已经调用完了,所以此时会打印后面的过滤器1 后置处理这句话。这样,最终打印的结果就是上面我们看到的那样了。

当然了,这里我只是简单地去模拟了一下FilterChain,目的就是希望大家再好好地理解一下责任链模式的思想,如果能理解个分毫,也算是没辜负我这片苦心了!

以上是关于从零开始学习Java设计模式 | 行为型模式篇:责任链模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:状态模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:命令模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式

从零开始学习Java设计模式 | 行为型模式篇:责任链模式