面试常考题 设计模式(六)-责任链设计模式

其实, 说到责任链设计模式, 我们平时使用的也真是挺多的. 比如: 天天用的网关过滤器, 我们请假的审批流, 打游戏通关, 我们写代码常用的日志打印. 他们都使用了责任链设计模式.

下面就来详细研究一下责任链设计模式

一. 什么是责任链设计模式?

官方定义:


责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。

在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

含义解析:


定义中提到的两个主体: 请求的发送者和请求的接收者. 用员工请假来举例. 请求发送者是员工, 请求接收者是主管们.
「对请求的发送者和接收者进行解耦」: 意思就是员工发起请假申请和主管审批请假解耦.
「为请求创建了一个接收者对象的链」: 意思是接收者有多个, 实现了多个接收者进行审批的链条.

二. 责任链设计模式的使用场景

  • 网关过滤器: 一个url请求过来, 首先要校验url是否是合法的, 不合法过滤掉, 合法进入下一层校验; 是否是在黑名单中, 如果在过滤掉,不在进行下一层校验; 校验参数是否合规, 不合规过滤掉, 合规进入下一层校验, 等等.
  • 请假审批流: 请假天数小于3天, 直属领导审批即可; 天数大于3天,小于10天, 要部门主管审批; 天数大于10天要总经理审批
  • 游戏通关: 完成第一关, 并且分数>90, 才能进入第二关; 完成第二关, 分数>80, 才能进入第三关等等
  • 日志处理: 日志的级别从小到大分别是: dubug, info ,warn, error .
    • console控制台: 控制台接收debug级别的日志, 那么所有debug, info, warn, error日志内容都打印在console控制台中.
    • file文件: file接收info级别的日志. 那么info, warn, error级别的日志都会打印到file文件中, 但是debug日志不会打印
    • error文件: 只接收error级别的日志, 其他界别的日志都不接收.

三. 责任链设计模式的实现思路

下面以一个简单的案例[请假审批流]来介绍责任链的实现

1. 需求:

有一个员工, 他要请求. 公司规定, 请假3天以内, 直属领导就可以审批. 请假3-10天, 需要部门经理审批. 请假大于10天需要总经理审批.

2. 通常实现方式

这个审批流, 我们第一想法是使用if....else....来写.

public void approve(Integer days) {
    if (days <= 3) {
        // 直属领导审批
    } else if (days > 3 && days <= 10) {
        // 部门经理审批
    } else if (days > 10) {
        // 总经理审批
    }
}

这样写确实可以实现。 但是他有几个缺点:

  1. 这个审批方法很长,一大段代码看起来并不美观。 这里看着代码很少,那是因为我没有具体实现审批逻辑, 当审批人很多的时候, if...else...也会很多,就会显得很臃肿了。
  2. 可扩展性差: 加入现在要在部门经理和总经理之间在家一个审批流。 我们要修改原来的代码,修改原来的代码,就有可能引入bug, 违背了开放-封闭原则。
  3. 违背单一职责原则:这个类承担了多个角色的多个责任,违背了单一职责原则。
  4. 不能跨级别审批:加入有一个特殊的人,他请假3天,也需要总经理审批,这个if...else....就没法实现了。

既然可能增加多个审批人,我们可以考虑将具体的审批人做成审批者的子类,利用多态来实现。

3. 责任链实现方式

第一步: 请假, 定义一个请假实体类LeaveRequest。这就是请求的发出者

@Data
public class LeaveRequest {
    /**
     * 请假的人
     */
    private String name;

    /**
     * 请假的天数
     */
    private int days;

    public LeaveRequest() {
    }

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

有两个属性, 谁请假(name), 请了几天(days).

第二步: 抽象请假审批者

/**
 * 抽象的请假处理类
 */
@Data
public abstract class LeaveHandler {
    /**
     * 处理人姓名
     */
    private String handlerName;

    /**
     * 下一个处理人
     */
    private LeaveHandler nextHandler;

    public void setNextHandler(LeaveHandler leaveHandler) {
        this.nextHandler = leaveHandler;
    }

    public LeaveHandler(String handlerName) {
        this.handlerName = handlerName;
    }

    /**
     * 具体的处理操作
     * @param leaveRequest
     * @return
     */
    public abstract boolean process(LeaveRequest leaveRequest);
}

这里定义了如下内容:

  1. 审批者姓名,
  2. 审批人要执行的操作process()方法。审批的内容是请假信息, 返回值是审批结果,通过或者不通过
  3. 下一个处理者nextHandler:这是重点。也是我们链条能够连续执行的关键。

第三步:定义具体的操作者

  • 直属领导处理类:DirectLeaveHandler.java

    /**
    * 天数小于3天, 直属领导处理
    */
    public class DirectLeaveHandler extends LeaveHandler{
      public DirectLeaveHandler(String directName) {
          super(directName);
      }
      @Override
      public boolean process(LeaveRequest leaveRequest) {
          // 随机数大于3则为批准,否则不批准
          boolean result = (new Random().nextInt(10)) > 3;
          if (!result) {
              System.out.println(this.getHandlerName() + "审批驳回");
              return false;
          } else if (leaveRequest.getDays() <= 3) {
              // 审批通过
              System.out.println(this.getHandlerName() + "审批完成");
              return true;
          } else{
              System.out.println(this.getHandlerName() + "审批完成");
              return this.getNextHandler().process(leaveRequest);
          }
      }
    }

    这里模拟了领导审批的流程. 如果小于3天, 直属领导直接审批, 可能通过, 可能不通过. 如果超过3天, 提交给下一级领导审批.

  • 部门经理处理类: ManagerLeaveHandler

    public class ManagerLeaveHandler extends LeaveHandler{
    
      public ManagerLeaveHandler(String name) {
          super(name);
      }
      @Override
      public boolean process(LeaveRequest leaveRequest) {
          // 随机数大于3则为批准,否则不批准
          boolean result = (new Random().nextInt(10)) > 3;
          if (!result) {
              System.out.println(this.getHandlerName() + "审批驳回");
              return false;
          } else if (leaveRequest.getDays() > 3 && leaveRequest.getDays() <= 10) {
              System.out.println(this.getHandlerName() + "审批完成");
              return true;
          } else {
              System.out.println(this.getHandlerName() + "审批完成");
              return this.getNextHandler().process(leaveRequest);
          }
      }
    }

    部门经理处理的是3-10天的假期, 如果超过10天, 还要交由下一级领导审批

    • 总经理处理类:
      public class GeneralManagerLeavHandler extends LeaveHandler{
       public GeneralManagerLeavHandler(String name) {
           super(name);
       }
       @Override
       public boolean process(LeaveRequest leaveRequest) {
           // 随机数大于3则为批准,否则不批准
           boolean result = (new Random().nextInt(10)) > 3;
           if (!result) {
               System.out.println(this.getHandlerName() + "审批驳回");
               return false;
           } else {
               System.out.println(this.getHandlerName() + "审批完成");
               return true;
           }
       }
      }
      左右最终流转到总经理的假期都会被审批

第四步: 定义客户端发起请求操作

    public static void main(String[] args) {
        DirectLeaveHandler directLeaveHandler = new DirectLeaveHandler("直属主管");
        ManagerLeaveHandler managerLeaveHandler = new ManagerLeaveHandler("部门经理");
        GeneralManagerLeavHandler generalManagerLeavHandler = new GeneralManagerLeavHandler("总经理");

        directLeaveHandler.setNextHandler(managerLeaveHandler);
        managerLeaveHandler.setNextHandler(generalManagerLeavHandler);

        System.out.println("========张三请假2天==========");
        LeaveRequest lxl = new LeaveRequest("张三", 2);
        directLeaveHandler.process(lxl);

        System.out.println("========李四请假6天==========");
        LeaveRequest wangxiao = new LeaveRequest("李四", 6);
        directLeaveHandler.process(wangxiao);


        System.out.println("========王五请假30天==========");
        LeaveRequest yongMing = new LeaveRequest("王五", 30);
        directLeaveHandler.process(yongMing);
    }

这里我们创建了一个直属领导, 一个部门经理,一个总经理. 并设置了上下级关系.
然后根据员工请假的天数来判断, 应该如何审批.
对于用户而言,他不需要知道前面有多少个领导需要审批. 他只需要提交给第一个领导, 也就是直属领导, 然后不断往下走审批就可以了. 也就是说,在责任链设计模式中,我们只需要拿到链上的第一个处理者,那么链上的每个处理者都有机会处理相应的请求。

以上代码基本上概括了责任链设计模式的使用,但是上述客户端的代码其实也是很繁琐的,后面我们会继续优化责任链设计模式。

第五步: 查看结果

由于请假是随机了, 还有可能被驳回. 我们先来看看全部同意的请求结果

========张三请假2天==========
直属主管审批完成
========李四请假6天==========
直属主管审批完成
部门经理审批完成
========王五请假30天==========
直属主管审批完成
部门经理审批完成
总经理审批完成

再来看看有驳回的请求结果

========张三请假2天==========
直属主管审批驳回
========李四请假6天==========
直属主管审批驳回
========王五请假30天==========
直属主管审批完成
部门经理审批驳回

4. 责任链概念抽象总结

责任链设计模式: 客户端发出一个请求,链上的对象都有机会来处理这一请求,而客户端不需要知道谁是具体的处理对象。多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。 将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止

上面的代码基本上概括了责任链设计模式的使用,但是上述客户端的代码其实也是很繁琐的,后面我优化责任链设计模式。

4. 责任链设计模式的优缺点

优点

动态组合,使请求者和接受者解耦。
请求者和接受者松散耦合:请求者不需要知道接受者,也不需要知道如何处理。每个职责对象只负责自己的职责范围,其他的交给后继者。各个组件间完全解耦。
动态组合职责:职责链模式会把功能分散到单独的职责对象中,然后在使用时动态的组合形成链,从而可以灵活的分配职责对象,也可以灵活的添加改变对象职责。

缺点

产生很多细粒度的对象:因为功能处理都分散到了单独的职责对象中,每个对象功能单一,要把整个流程处理完,需要很多的职责对象,会产生大量的细粒度职责对象。
不一定能处理:每个职责对象都只负责自己的部分,这样就可以出现某个请求,即使把整个链走完,都没有职责对象处理它。这就需要提供默认处理,并且注意构造链的有效性。

四. 综合案例 -- 网关权限控制

1. 明确需求

网关有很多功能: API接口限流, 黑名单拦截, 权限验证, 参数过滤等. 下面我们就通过责任链设计模式来实现网关权限控制。

2. 实现思路

来看一下下面的类图.

可以看到定义了一个抽象的网关处理器. 然后有4个子处理器的实现类.

3. 具体实现

第一步: 定义抽象的网关处理器类

/**
 * 定义抽象的网关处理器类
 */
public abstract class AbstractGatewayHandler {
    /**
     * 定义下一个网关处理器
     */
    protected AbstractGatewayHandler nextGatewayHandler;

    public void setNextGatewayHandler(AbstractGatewayHandler nextGatewayHandler) {
        this.nextGatewayHandler = nextGatewayHandler;
    }

    /**
     * 抽象网关执行的服务
     * @param url
     */
    public abstract void service(String url);
}

第二步: 定义具体的网关服务

1. API接口限流处理器

/**
 * API接口限流处理器
 */
public class APILimitGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("api接口限流处理, 处理完成");
        // 实现具体的限流服务流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

2. 黑名单拦截处理器

/**
 * 黑名单处理器
 */
public class BlankListGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("黑名单处理, 处理完成");

        // 实现具体的限流服务流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

3. 权限验证处理器

/**
 * 权限验证处理器
 */
public class PermissionValidationGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("权限验证处理, 处理完成");
        // 实现具体的限流服务流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

4. 参数校验处理器

/**
 * 参数校验处理器
 */
public class ParameterVerificationGatewayHandler extends AbstractGatewayHandler {
    @Override
    public void service(String url) {
        System.out.println("参数校验处理, 处理完成");
        // 实现具体的限流服务流程
        if (this.nextGatewayHandler != null) {
            this.nextGatewayHandler.service(url);
        }
    }
}

第三步: 定义网关客户端, 设置网关请求链

/**
 * 网关客户端
 */
public class GatewayClient {
    public static void main(String[] args) {
        APILimitGatewayHandler apiLimitGatewayHandler = new APILimitGatewayHandler();
        BlankListGatewayHandler blankListGatewayHandler = new BlankListGatewayHandler();
        ParameterVerificationGatewayHandler parameterVerificationGatewayHandler = new ParameterVerificationGatewayHandler();
        PermissionValidationGatewayHandler permissionValidationGatewayHandler = new PermissionValidationGatewayHandler();

        apiLimitGatewayHandler.setNextGatewayHandler(blankListGatewayHandler);
        blankListGatewayHandler.setNextGatewayHandler(parameterVerificationGatewayHandler);
        parameterVerificationGatewayHandler.setNextGatewayHandler(permissionValidationGatewayHandler);

        apiLimitGatewayHandler.service("http://www.baidu.com");
    }
}

这里和之前差不多, 不做太多解释了, 来看运行效果:

api接口限流处理, 处理完成
黑名单处理, 处理完成
参数校验处理, 处理完成
权限验证处理, 处理完成

这样就进行了一系列的网关处理. 当然, 每一次处理都应该返回处理结果, 然后决定是否进行下一次处理. 这里就简化了

第四步: 使用工厂模式优化责任链设计模式

在第三步网关客户端中,对责任链进行了初始化操作。 这样, 每次客户端想要发起请求都需要执行一遍初始化操作, 其实完全没有这个必要. 我们可以使用工厂设计模式, 将客户端抽取到工厂中, 每次只需要拿到链上的第一个处理者就可以了.

1. 定义网关处理器工厂

/**
 * 网关处理器工厂
 */
public class GatewayHandlerFactory {
    public static AbstractGatewayHandler getFirstGatewayHandler() {
        APILimitGatewayHandler apiLimitGatewayHandler = new APILimitGatewayHandler();
        BlankListGatewayHandler blankListGatewayHandler = new BlankListGatewayHandler();
        ParameterVerificationGatewayHandler parameterVerificationGatewayHandler = new ParameterVerificationGatewayHandler();
        PermissionValidationGatewayHandler permissionValidationGatewayHandler = new PermissionValidationGatewayHandler();

        apiLimitGatewayHandler.setNextGatewayHandler(blankListGatewayHandler);
        blankListGatewayHandler.setNextGatewayHandler(parameterVerificationGatewayHandler);
        parameterVerificationGatewayHandler.setNextGatewayHandler(permissionValidationGatewayHandler);

        return apiLimitGatewayHandler;
    }
}

网关处理器工厂定义了各个网关处理器之间的关系, 并返回第一个网关处理器.

2.优化网关客户端

/**
 * 网关客户端
 */
public class GatewayClient {
    public static void main(String[] args) {
        GatewayHandlerFactory.getFirstGatewayHandler().service("http://www.baidu.com");
    }
}

我们在客户端只需要直接调用第一个网关处理器就可以了, 不需要关心其他的处理器.

五. 责任链模式总结

  1. 定义一个抽象的父类, 在抽象的父类中定义请求处理的方法 和 下一个处理者.
  2. 然后子类处理器继承分类处理器, 并实现自己的请求处理方法
  3. 设置处理请求链, 可以采用工厂设计模式抽象, 请求者只需要知道整个链条的第一环
#Java研发工程师实习##面试题目#
全部评论
很常用的设计模式
点赞 回复 分享
发布于 2021-07-11 06:54
感谢参与【创作者计划3期·技术干货场】!欢迎更多牛油来写干货,瓜分总计20000元奖励!!技术干货场活动链接:https://www.nowcoder.com/link/czz3jsgh3(参与奖马克杯将于每周五结算,敬请期待~)
点赞 回复 分享
发布于 2021-07-12 18:23

相关推荐

鼗:四级有点难绷,感觉能拿国家励志奖学金,学习能力应该蛮强的,四级确实不重要,但是拿这个卡你可是很恶心啊
点赞 评论 收藏
分享
1 4 评论
分享
牛客网
牛客企业服务