面试官:在项目中用过责任链模式吗?
一、开篇
阅读本文可以了解哪些知识?
结合具体案例,领略责任链模式的魅力。
责任链模式实现流程编排、动态扩展。
使用 Sping @Resource 注解注入的骚操作。
使用递归算法设置责任链路。
二、简介
责任链模式,简而言之,就是将多个操作组装成一条链路进行处理。请求在链路上传递,链路上的每一个节点就是一个处理器,每个处理器都可以对请求进行处理,或者传递给链路上的下一个处理器处理。

三、应用场景
责任链模式的应用场景,在实际工作中,通常有如下两种应用场景。
操作需要经过一系列的校验,通过校验后才执行某些操作。
工作流。企业中通常会制定很多工作流程,一级一级的去处理任务。
下面通过两个案例来学习一下责任链模式。
案例一:创建商品多级校验场景
以创建商品为例,假设商品创建逻辑分为以下三步完成:①创建商品、②校验商品参数、③保存商品。
第②步校验商品又分为多种情况的校验,必填字段校验、规格校验、价格校验、库存校验等等。这些检验逻辑像一个流水线,要想创建出一个商品,必须通过这些校验。如下流程图所示:

伪代码如下:
创建商品步骤,需要经过一系列的参数校验,如果参数校验失败,直接返回失败的结果;通过所有的参数校验后,最终保存商品信息。

如上代码看起来似乎没什么问题,它非常工整,而且代码逻辑很清晰。
PS:我没有把所有的校验代码都罗列在一个方法里,那样更能产生对比性,但我觉得抽象并分离单一职责的函数应该是每个程序员最基本的规范!
但是随着业务需求不断地叠加,相关的校验逻辑也越来越多,新的功能使代码越来越臃肿,可维护性较差。更糟糕的是,这些校验组件不可复用,当你有其他需求也需要用到一些校验时,你又变成了 Ctrl+C , Ctrl+V 程序员,系统的维护成本也越来越高。如下图所示:

伪代码同上,这里就不赘述了。
终于有一天,你忍无可忍了,决定重构这段代码。
使用责任链模式优化 :创建商品的每个校验步骤都可以作为一个单独的处理器,抽离为一个单独的类,便于复用。这些处理器形成一条链式调用,请求在处理器链上传递,如果校验条件不通过,则处理器不再向下传递请求,直接返回错误信息;若所有的处理器都通过检验,则执行保存商品步骤。

图片
案例一实战:责任链模式实现创建商品校验
UML 图:一览众山小

AbstractCheckHandler 表示处理器抽象类,负责抽象处理器行为。其有 3 个子类,分别是:
NullValueCheckHandler:空值校验处理器
PriceCheckHandler:价格校验处理
StockCheckHandler:库存校验处理器
AbstractCheckHandler 抽象类中, handle()
定义了处理器的抽象方法,其子类需要重写handle()
方法以实现特殊的处理器校验逻辑;
protected ProductCheckHandlerConfig config 是处理器的动态配置类,使用 protected 声明,每个子类处理器都持有该对象。该对象用于声明当前处理器、以及当前处理器的下一个处理器 nextHandler,另外也可以配置一些特殊属性,比如说接口降级配置、超时时间配置等。
AbstractCheckHandler nextHandler 是当前处理器持有的下一个处理器的引用,当前处理器执行完毕时,便调用 nextHandler 执行下一处理器的 handle()校验方法;
protected Result next()
是抽象类中定义的,执行下一个处理器的方法,使用 protected 声明,每个子类处理器都持有该对象。当子类处理器执行完毕(通过)时,调用父类的方法执行下一个处理器 nextHandler。
HandlerClient 是执行处理器链路的客户端,HandlerClient.executeChain()
方法负责发起整个链路调用,并接收处理器链路的返回值。
撸起袖子开始撸代码吧 🤓 ~
商品参数对象:保存商品的入参
ProductVO 是创建商品的参数对象,包含商品的基础信息。并且其作为责任链模式中多个处理器的入参,多个处理器都以 ProductVO 为入参进行特定的逻辑处理。实际业务中,商品对象特别复杂。咱们化繁为简,简化商品参数如下:
抽象类处理器:抽象行为,子类共有属性、方法
AbstractCheckHandler :处理器抽象类,并使用 @Component 注解注册为由 Spring 管理的 Bean 对象,这样做的好处是,我们可以轻松的使用 Spring 来管理这些处理器 Bean。
在 AbstractCheckHandler 抽象类处理器中,使用 protected 声明子类可见的属性和方法。使用 @Component 注解,声明其为 Spring 的 Bean 对象,这样做的好处是可以利用 Spring 轻松管理所有的子类,下面会看到如何使用。抽象类的属性和方法说明如下:
public abstract Result handle():表示抽象的校验方法,每个处理器都应该继承 AbstractCheckHandler 抽象类处理器,并重写其 handle 方法,各个处理器从而实现特殊的校验逻辑,实际上就是多态的思想。
protected ProductCheckHandlerConfig config:表示每个处理器的动态配置类,可以通过“配置中心”动态修改该配置,实现处理器的“动态编排”和“顺序控制”。配置类中可以配置处理器的名称、下一个处理器、以及处理器是否降级等属性。
protected AbstractCheckHandler nextHandler:表示当前处理器持有下一个处理器的引用,如果当前处理器 handle()校验方法执行完毕,则执行下一个处理器 nextHandler 的 handle()校验方法执行校验逻辑。
protected Result next(ProductVO param):此方法用于处理器链路传递,子类处理器执行完毕后,调用父类的 next()方法执行在 config 配置的链路上的下一个处理器,如果所有处理器都执行完毕了,就返回结果了。
ProductCheckHandlerConfig 配置类 :
子类处理器:处理特有的校验逻辑
AbstractCheckHandler 抽象类处理器有 3 个子类分别是:
NullValueCheckHandler:空值校验处理器
PriceCheckHandler:价格校验处理
StockCheckHandler:库存校验处理器
各个处理器继承 AbstractCheckHandler 抽象类处理器,并重写其 handle()处理方法以实现特有的校验逻辑。
NullValueCheckHandler :空值校验处理器。针对性校验创建商品中必填的参数。如果校验未通过,则返回错误码 ErrorCode,责任链在此截断(停止),创建商品返回被校验住的错误信息。注意代码中的降级配置!
super.getConfig().getDown()
是获取 AbstractCheckHandler 处理器对象中保存的配置信息,如果处理器配置了降级,则跳过该处理器,调用super.next()
执行下一个处理器逻辑。
同样,使用 @Component 注册为由 Spring 管理的 Bean 对象,
PriceCheckHandler :价格校验处理。针对创建商品的价格参数进行校验。这里只是做了简单的判断价格>0 的校验,实际业务中比较复杂,比如“价格门”这些防范措施等。
StockCheckHandler :库存校验处理器。针对创建商品的库存参数进行校验。
客户端:执行处理器链路
HandlerClient 客户端类负责发起整个处理器链路的执行,通过executeChain()
方法。如果处理器链路返回错误信息,即校验未通过,则整个链路截断(停止),返回相应的错误信息。
以上,责任链模式相关的类已经创建好了。接下来就可以创建商品了。
创建商品:抽象步骤,化繁为简
createProduct()
创建商品方法抽象为 2 个步骤:①参数校验、②创建商品。参数校验使用责任链模式进行校验,包含:空值校验、价格校验、库存校验等等,只有链上的所有处理器均校验通过,才调用saveProduct()
创建商品方法;否则返回校验错误信息。
在createProduct()
创建商品方法中,通过责任链模式,我们将校验逻辑进行解耦。createProduct()
创建商品方法中不需要关注都要经过哪些校验处理器,以及校验处理器的细节。
参数校验:责任链模式
参数校验paramCheck()
方法使用责任链模式进行参数校验,方法内没有声明具体都有哪些校验,具体有哪些参数校验逻辑是通过多个处理器链传递的。如下:
paramCheck()
方法步骤说明如下:
👉 步骤 1:获取处理器配置。
通过getHandlerConfigFile()
方法获取处理器配置类对象,配置类保存了链上各个处理器的上下级节点配置,支持流程编排、动态扩展。通常配置是通过 Ducc(京东自研的配置中心)、Nacos(阿里开源的配置中心)等配置中心存储的,支持动态变更、实时生效。
基于此,我们便可以实现校验处理器的编排、以及动态扩展了。我这里没有使用配置中心存储处理器链路的配置,而是使用 JSON 串的形式去模拟配置,大家感兴趣的可以自行实现。
ConfigJson 存储的处理器链路配置 JSON 串,在代码中可能不便于观看,我们可以使用 json.cn 等格式化看一下,如下,配置的整个调用链路规则特别清晰。

getHandlerConfigFile()
类获到配置类的结构如下,可以看到,就是把在配置中心储存的配置规则,转换成配置类ProductCheckHandlerConfig
对象,用于程序处理。
注意,此时配置类中存储的仅仅是处理器 Spring Bean 的 name 而已,并非实际处理器对象。

接下来,通过配置类获取实际要执行的处理器。
👉 步骤 2:根据配置获取处理器。
上面步骤 1 通过getHandlerConfigFile()
方法获取到处理器链路配置规则后,再调用getHandler()
获取处理器。
getHandler()
参数是如上 ConfigJson 配置的规则,即步骤 1 转换成的ProductCheckHandlerConfig
对象;根据ProductCheckHandlerConfig
配置规则转换成处理器链路对象。代码如下:
👉 👉 步骤 2-1:配置检查。
代码 14~27 行,进行了配置的一些检查操作。如果配置错误,则获取不到对应的处理器。代码 23 行handlerMap.get(config.getHandler())
是从所有处理器映射 Map 中获取到对应的处理器 Spring Bean。
注意第 5 行代码,handlerMap 存储了所有的处理器映射,是通过 Spring @Resource 注解注入进来的。注入的规则是:所有继承了 AbstractCheckHandler 抽象类(它是 Spring 管理的 Bean)的子类(子类也是 Spring 管理的 Bean)都会注入进来。
注入进来的 handlerMap 中 Map 的 Key 对应 Bean 的 name,Value 是 name 对应的 Bean 实例,也就是实际的处理器,这里指空值校验处理器、价格校验处理器、库存校验处理器。如下:

这样根据配置 ConfigJson(👉 步骤 1:获取处理器配置)中handler:"priceCheckHandler"
的配置,使用handlerMap.get(config.getHandler())
便可以获取到对应的处理器 Spring Bean 对象了。
👉 👉 步骤 2-2:保存处理器规则。
代码 29 行,将配置规则保存到对应的处理器中abstractCheckHandler.setConfig(config)
,子类处理器就持有了配置的规则。
👉 👉 步骤 2-3:递归设置处理器链路。
代码 32 行,递归设置链路上的处理器。
这一步可能不太好理解,结合 ConfigJson 配置的规则来看,似乎就很很容易理解了。

由上而下,NullValueCheckHandler
空值校验处理器通过setNextHandler()
方法设置自己持有的下一节点的处理器,也就是价格处理器 PriceCheckHandler。
接着,PriceCheckHandler 价格处理器,同样需要经过步骤 2-1 配置检查、步骤 2-2 保存配置规则,并且最重要的是,它也需要设置下一节点的处理器 StockCheckHandler 库存校验处理器。
StockCheckHandler 库存校验处理器也一样,同样需要经过步骤 2-1 配置检查、步骤 2-2 保存配置规则,但请注意 StockCheckHandler 的配置,它的 next 规则配置了 null,这表示它下面没有任何处理器要执行了,它就是整个链路上的最后一个处理节点。
通过递归调用getHandler()
获取处理器方法,就将整个处理器链路对象串联起来 了。如下:

友情提示:递归虽香,但使用递归一定要注意截断递归的条件处理,否则可能造成死循环哦!
实际上,getHandler()
获取处理器对象的代码就是把在配置中心配置的规则 ConfigJson,转换成配置类ProductCheckHandlerConfig
对象,再根据配置类对象,转换成实际的处理器对象,这个处理器对象持有整个链路的调用顺序。
👉 步骤 3:客户端执行调用链路。
getHandler()获取完处理器后,整个调用链路的执行顺序也就确定了,此时,客户端该干活了!
HandlerClient.executeChain(handler, param)
方法是 HandlerClient 客户端类执行处理器整个调用链路的,并接收处理器链路的返回值。
executeChain()
通过AbstractCheckHandler.handle()
触发整个链路处理器顺序执行,如果某个处理器校验没有通过!handlerResult.isSuccess()
,则返回错误信息;所有处理器都校验通过,则返回正确信息Result.success()
。
总结:串联方法调用流程
基于以上,再通过流程图来回顾一下整个调用流程。

测试:代码执行结果
场景 1:创建商品参数中有空值(如下 skuId 参数为 null),链路被空值处理器截断,返回错误信息
测试结果

场景 2:创建商品价格参数异常(如下 price 参数),被价格处理器截断,返回错误信息
测试结果

场景 3:创建商品库存参数异常(如下 stock 参数),被库存处理器截断,返回错误信息。
测试结果

场景 4:创建商品所有处理器校验通过,保存商品。
测试结果

案例二:工作流,费用报销审核流程
同事小贾最近刚出差回来,她迫不及待的就提交了费用报销的流程。根据金额不同,分为以下几种审核流程。报销金额低于 1000 元,三级部门管理者审批即可,1000 到 5000 元除了三级部门管理者审批,还需要二级部门管理者审批,而 5000 到 10000 元还需要一级部门管理者审批。即有以下几种情况:
小贾需报销 500 元,三级部门管理者审批即可。
小贾需报销 2500 元,三级部门管理者审批通过后,还需要二级部门管理者审批,二级部门管理者审批通过后,才完成报销审批流程。
小贾需报销 7500 元,三级管理者审批通过后,并且二级管理者审批通过后,流程流转到一级部门管理者进行审批,一级管理者审批通过后,即完成了报销流程。

UML 图
AbstractFlowHandler 作为处理器抽象类,抽象了approve()
审核方法,一级、二级、三级部门管理者处理器继承了抽象类,并重写其approve()
审核方法,从而实现特有的审核逻辑。

配置类如下所示,每层的处理器都要配置审核人、价格审核规则(审核的最大、最小金额)、下一级处理人。配置规则是可以动态变更的,如果三级部门管理者可以审核的金额增加到 2000 元,修改一下配置即可动态生效。

代码实现与案例一相似,感兴趣的自己动动小手吧~
四、责任链的优缺点


评论