面试常考题 设计模式(六)-责任链设计模式
其实, 说到责任链设计模式, 我们平时使用的也真是挺多的. 比如: 天天用的网关过滤器, 我们请假的审批流, 打游戏通关, 我们写代码常用的日志打印. 他们都使用了责任链设计模式.
下面就来详细研究一下责任链设计模式
一. 什么是责任链设计模式?
官方定义:
责任链模式(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) { // 总经理审批 } }
这样写确实可以实现。 但是他有几个缺点:
- 这个审批方法很长,一大段代码看起来并不美观。 这里看着代码很少,那是因为我没有具体实现审批逻辑, 当审批人很多的时候, if...else...也会很多,就会显得很臃肿了。
- 可扩展性差: 加入现在要在部门经理和总经理之间在家一个审批流。 我们要修改原来的代码,修改原来的代码,就有可能引入bug, 违背了开放-封闭原则。
- 违背单一职责原则:这个类承担了多个角色的多个责任,违背了单一职责原则。
- 不能跨级别审批:加入有一个特殊的人,他请假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); }
这里定义了如下内容:
- 审批者姓名,
- 审批人要执行的操作process()方法。审批的内容是请假信息, 返回值是审批结果,通过或者不通过
- 下一个处理者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"); } }
我们在客户端只需要直接调用第一个网关处理器就可以了, 不需要关心其他的处理器.
五. 责任链模式总结
- 定义一个抽象的父类, 在抽象的父类中定义请求处理的方法 和 下一个处理者.
- 然后子类处理器继承分类处理器, 并实现自己的请求处理方法
- 设置处理请求链, 可以采用工厂设计模式抽象, 请求者只需要知道整个链条的第一环