职责链形式,简而言之,便是将多个操作组装成一条链路进行处理。恳求在链路上传递,链路上的每一个节点便是一个处理器,每个处理器都能够对恳求进行处理,或许传递给链路上的下一个处理器处理。
文章首发大众号:码猿技能专栏;作者:不才陈某
运用场景
职责链形式的运用场景,在实践作业中,一般有如下两种运用场景。
- 操作需求经过一系列的校验,经过校验后才履行某些操作。
- 作业流。企业中一般会制定许多作业流程,一级一级的去处理使命。
下面经过两个案例来学习一下职责链形式。
案例一:创立产品多级校验场景
以创立产品为例,假定产品创立逻辑分为以下三步完成:①创立产品、②校验产品参数、③保存产品。
第②步校验产品又分为多种状况的校验,必填字段校验、标准校验、价格校验、库存校验等等。这些检验逻辑像一个流水线,要想创立出一个产品,必须经过这些校验。如下流程图所示:
伪代码如下:
创立产品过程,需求经过一系列的参数校验,假如参数校验失利,直接回来失利的成果;经过一切的参数校验后,最终保存产品信息。
如上代码看起来好像没什么问题,它非常工整,而且代码逻辑很明晰。
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为入参进行特定的逻辑处理。实践事务中,产品目标特别复杂。咱们化繁为简,简化产品参数如下:
/**
* 产品目标
*/
@Data
@Builder
public class ProductVO {
/**
* 产品SKU,仅有
*/
private Long skuId;
/**
* 产品称号
*/
private String skuName;
/**
* 产品图片路径
*/
private String Path;
/**
* 价格
*/
private BigDecimal price;
/**
* 库存
*/
private Integer stock;
}
笼统类处理器:笼统行为,子类共有特点、办法
AbstractCheckHandler:处理器笼统类,并运用@Component注解注册为由Spring办理的Bean目标,这样做的好处是,咱们能够轻松的运用Spring来办理这些处理器Bean。
/**
* 笼统类处理器
*/
@Component
public abstract class AbstractCheckHandler {
/**
* 当时处理器持有下一个处理器的引用
*/
@Getter
@Setter
protected AbstractCheckHandler nextHandler;
/**
* 处理器装备
*/
@Setter
@Getter
protected ProductCheckHandlerConfig config;
/**
* 处理器履行办法
* @param param
* @return
*/
public abstract Result handle(ProductVO param);
/**
* 链路传递
* @param param
* @return
*/
protected Result next(ProductVO param) {
//下一个链路没有处理器了,直接回来
if (Objects.isNull(nextHandler)) {
return Result.success();
}
//履行下一个处理器
return nextHandler.handle(param);
}
}
在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装备类 :
/**
* 处理器装备类
*/
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
/**
* 处理器Bean称号
*/
private String handler;
/**
* 下一个处理器
*/
private ProductCheckHandlerConfig next;
/**
* 是否降级
*/
private Boolean down = Boolean.FALSE;
}
子类处理器:处理特有的校验逻辑
AbstractCheckHandler笼统类处理器有3个子类分别是:
- NullValueCheckHandler:空值校验处理器
- PriceCheckHandler:价格校验处理
- StockCheckHandler:库存校验处理器
各个处理器承继AbstractCheckHandler笼统类处理器,并重写其handle()处理办法以完成特有的校验逻辑。
NullValueCheckHandler:空值校验处理器。针对性校验创立产品中必填的参数。假如校验未经过,则回来过错码ErrorCode,职责链在此切断(中止),创立产品回来被校验住的过错信息。留意代码中的降级装备!
super.getConfig().getDown()
是获取AbstractCheckHandler处理器目标中保存的装备信息,假如处理器装备了降级,则越过该处理器,调用super.next()
履行下一个处理器逻辑。
相同,运用@Component注册为由Spring办理的Bean目标,
/**
* 空值校验处理器
*/
@Component
public class NullValueCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("空值校验 Handler 开端...");
//降级:假如装备了降级,则越过此处理器,履行下一个处理器
if (super.getConfig().getDown()) {
System.out.println("空值校验 Handler 已降级,越过空值校验 Handler...");
return super.next(param);
}
//参数必填校验
if (Objects.isNull(param)) {
return Result.failure(ErrorCode.PARAM_NULL_ERROR);
}
//SkuId产品主键参数必填校验
if (Objects.isNull(param.getSkuId())) {
return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
}
//Price价格参数必填校验
if (Objects.isNull(param.getPrice())) {
return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
}
//Stock库存参数必填校验
if (Objects.isNull(param.getStock())) {
return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
}
System.out.println("空值校验 Handler 经过...");
//履行下一个处理器
return super.next(param);
}
}
PriceCheckHandler:价格校验处理。针对创立产品的价格参数进行校验。这儿仅仅做了简单的判别价格>0的校验,实践事务中比较复杂,比方“价格门”这些防范措施等。
/**
* 价格校验处理器
*/
@Component
public class PriceCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("价格校验 Handler 开端...");
//不合法价格校验
boolean illegalPrice = param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
if (illegalPrice) {
return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
}
//其他校验逻辑...
System.out.println("价格校验 Handler 经过...");
//履行下一个处理器
return super.next(param);
}
}
StockCheckHandler:库存校验处理器。针对创立产品的库存参数进行校验。
/**
* 库存校验处理器
*/
@Component
public class StockCheckHandler extends AbstractCheckHandler{
@Override
public Result handle(ProductVO param) {
System.out.println("库存校验 Handler 开端...");
//不合法库存校验
boolean illegalStock = param.getStock() < 0;
if (illegalStock) {
return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
}
//其他校验逻辑..
System.out.println("库存校验 Handler 经过...");
//履行下一个处理器
return super.next(param);
}
}
客户端:履行处理器链路
HandlerClient客户端类担任建议整个处理器链路的履行,经过executeChain()
办法。假如处理器链路回来过错信息,即校验未经过,则整个链路切断(中止),回来相应的过错信息。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//履行处理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 职责链履行失利回来:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}
以上,职责链形式相关的类已经创立好了。接下来就能够创立产品了。
创立产品:笼统过程,化繁为简
createProduct()
创立产品办法笼统为2个过程:①参数校验、②创立产品。参数校验运用职责链形式进行校验,包括:空值校验、价格校验、库存校验等等,只有链上的一切处理器均校验经过,才调用saveProduct()
创立产品办法;否则回来校验过错信息。
在createProduct()
创立产品办法中,经过职责链形式,咱们将校验逻辑进行解耦。createProduct()
创立产品办法中不需求关注都要经过哪些校验处理器,以及校验处理器的细节。
/**
* 创立产品
* @return
*/
@Test
public Result createProduct(ProductVO param) {
//参数校验,运用职责链形式
Result paramCheckResult = this.paramCheck(param);
if (!paramCheckResult.isSuccess()) {
return paramCheckResult;
}
//创立产品
return this.saveProduct(param);
}
参数校验:职责链形式
参数校验paramCheck()
办法运用职责链形式进行参数校验,办法内没有声明具体都有哪些校验,具体有哪些参数校验逻辑是经过多个处理器链传递的。如下:
/**
* 参数校验:职责链形式
* @param param
* @return
*/
private Result paramCheck(ProductVO param) {
//获取处理器装备:一般装备运用一致装备中心存储,支撑动态改变
ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();
//获取处理器
AbstractCheckHandler handler = this.getHandler(handlerConfig);
//职责链:履行处理器链路
Result executeChainResult = HandlerClient.executeChain(handler, param);
if (!executeChainResult.isSuccess()) {
System.out.println("创立产品 失利...");
return executeChainResult;
}
//处理器链路全部成功
return Result.success();
}
paramCheck()
办法过程说明如下:
过程1:获取处理器装备。
经过getHandlerConfigFile()
办法获取处理器装备类目标,装备类保存了链上各个处理器的上下级节点装备,支撑流程编排、动态扩展。一般装备是经过Ducc(京东自研的装备中心)、Nacos(阿里开源的装备中心)等装备中心存储的,支撑动态改变、实时收效。
根据此,咱们便能够完成校验处理器的编排、以及动态扩展了。我这儿没有运用装备中心存储处理器链路的装备,而是运用JSON串的形式去模仿装备,我们感兴趣的能够自行完成。
/**
* 获取处理器装备:一般装备运用一致装备中心存储,支撑动态改变
* @return
*/
private ProductCheckHandlerConfig getHandlerConfigFile() {
//装备中心存储的装备
String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
//转成Config目标
ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
return handlerConfig;
}
ConfigJson存储的处理器链路装备JSON串,在代码中或许不便于观看,咱们能够运用json.cn等格式化看一下,如下,装备的整个调用链路规矩特别明晰。
getHandlerConfigFile()
类获到装备类的结构如下,能够看到,便是把在装备中心贮存的装备规矩,转换成装备类ProductCheckHandlerConfig
目标,用于程序处理。
留意,此时装备类中存储的仅仅是处理器Spring Bean的name而已,并非实践处理器目标。
接下来,经过装备类获取实践要履行的处理器。
过程2:依据装备获取处理器。
上面过程1经过getHandlerConfigFile()
办法获取到处理器链路装备规矩后,再调用getHandler()
获取处理器。
getHandler()
参数是如上ConfigJson装备的规矩,即过程1转换成的ProductCheckHandlerConfig
目标;依据ProductCheckHandlerConfig
装备规矩转换成处理器链路目标。代码如下:
![8](C:\Users\18796\Desktop\文章\8.png)![8](C:\Users\18796\Desktop\文章\8.png)![8](C:\Users\18796\Desktop\文章\8.png)/**
* 运用Spring注入:一切承继了AbstractCheckHandler笼统类的Spring Bean都会注入进来。Map的Key对应Bean的name,Value是name对应相应的Bean
*/
@Resource
private Map<String, AbstractCheckHandler> handlerMap;
/**
* 获取处理器
* @param config
* @return
*/
private AbstractCheckHandler getHandler (ProductCheckHandlerConfig config) {
//装备查看:没有装备处理器链路,则不履行校验逻辑
if (Objects.isNull(config)) {
return null;
}
//装备过错
String handler = config.getHandler();
if (StringUtils.isBlank(handler)) {
return null;
}
//装备了不存在的处理器
AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
if (Objects.isNull(abstractCheckHandler)) {
return null;
}
//处理器设置装备Config
abstractCheckHandler.setConfig(config);
//递归设置链路处理器
abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
return abstractCheckHandler;
}
过程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行,递归设置链路上的处理器。
//递归设置链路处理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));
这一步或许不太好理解,结合ConfigJson装备的规矩来看,好像就很很容易理解了。
由上而下,NullValueCheckHandler
空值校验处理器经过setNextHandler()
办法设置自己持有的下一节点的处理器,也便是价格处理器PriceCheckHandler。
接着,PriceCheckHandler价格处理器,相同需求经过过程2-1装备查看、过程2-2保存装备规矩,而且最重要的是,它也需求设置下一节点的处理器StockCheckHandler库存校验处理器。
StockCheckHandler库存校验处理器也相同,相同需求经过过程2-1装备查看、过程2-2保存装备规矩,但请留意StockCheckHandler的装备,它的next规矩装备了null,这表明它下面没有任何处理器要履行了,它便是整个链路上的最后一个处理节点。
经过递归调用getHandler()
获取处理器办法,就将整个处理器链路目标串联起来了。如下:
友谊提示:递归虽香,但运用递归一定要留意切断递归的条件处理,否则或许形成死循环哦!
实践上,getHandler()
获取处理器目标的代码便是把在装备中心装备的规矩ConfigJson,转换成装备类ProductCheckHandlerConfig
目标,再依据装备类目标,转换成实践的处理器目标,这个处理器目标持有整个链路的调用次序。
过程3:客户端履行调用链路。
public class HandlerClient {
public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
//履行处理器
Result handlerResult = handler.handle(param);
if (!handlerResult.isSuccess()) {
System.out.println("HandlerClient 职责链履行失利回来:" + handlerResult.toString());
return handlerResult;
}
return Result.success();
}
}
getHandler()获取完处理器后,整个调用链路的履行次序也就确认了,此时,客户端该干活了!
HandlerClient.executeChain(handler, param)
办法是HandlerClient客户端类履行处理器整个调用链路的,并接收处理器链路的回来值。
executeChain()
经过AbstractCheckHandler.handle()
触发整个链路处理器次序履行,假如某个处理器校验没有经过!handlerResult.isSuccess()
,则回来过错信息;一切处理器都校验经过,则回来正确信息Result.success()
。
总结:串联办法调用流程
根据以上,再经过流程图来回顾一下整个调用流程。
测验:代码履行成果
场景1:创立产品参数中有空值(如下skuId参数为null),链路被空值处理器切断,回来过错信息
//创立产品参数
ProductVO param = ProductVO.builder()
.skuId(null).skuName("华为手机").Path("http://...")
.price(new BigDecimal(1))
.stock(1)
.build();
测验成果
场景2:创立产品价格参数反常(如下price参数),被价格处理器切断,回来过错信息
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("华为手机").Path("http://...")
.price(new BigDecimal(-999))
.stock(1)
.build();
测验成果
场景 3:创立产品库存参数反常(如下stock参数),被库存处理器切断,回来过错信息。
//创立产品参数,模仿用户传入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("华为手机").Path("http://...")
.price(new BigDecimal(1))
.stock(-999)
.build();
测验成果
场景4:创立产品一切处理器校验经过,保存产品。
![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)![15](C:\Users\18796\Desktop\文章\15.png)//创立产品参数,模仿用户传入
ProductVO param = ProductVO.builder()
.skuId(1L).skuName("华为手机").Path("http://...")
.price(new BigDecimal(999))
.stock(1).build();
测验成果
案例二:作业流,费用报销审阅流程
同事小贾最近刚出差回来,她迫不及待的就提交了费用报销的流程。依据金额不同,分为以下几种审阅流程。报销金额低于1000元,三级部分办理者批阅即可,1000到5000元除了三级部分办理者批阅,还需求二级部分办理者批阅,而5000到10000元还需求一级部分办理者批阅。即有以下几种状况:
- 小贾需报销500元,三级部分办理者批阅即可。
- 小贾需报销2500元,三级部分办理者批阅经往后,还需求二级部分办理者批阅,二级部分办理者批阅经往后,才完成报销批阅流程。
- 小贾需报销7500元,三级办理者批阅经往后,而且二级办理者批阅经往后,流程流转到一级部分办理者进行批阅,一级办理者批阅经往后,即完成了报销流程。
UML图
AbstractFlowHandler作为处理器笼统类,笼统了approve()
审阅办法,一级、二级、三级部分办理者处理器承继了笼统类,并重写其approve()
审阅办法,然后完成特有的审阅逻辑。
装备类如下所示,每层的处理器都要装备审阅人、价格审阅规矩(审阅的最大、最小金额)、下一级处理人。装备规矩是能够动态改变的,假如三级部分办理者能够审阅的金额增加到2000元,修正一下装备即可动态收效。
代码完成与案例一类似,感兴趣的自己动动小手吧~
职责链的优缺点
源码查看
地址:github.com/rongtao7/My…