关于后端:责任链模式的高级用法多级校验工作流这样写代码才足够优雅

2次阅读

共计 11744 个字符,预计需要花费 30 分钟才能阅读完成。

利用场景
责任链模式的利用场景,在理论工作中,通常有如下两种利用场景。

操作须要通过一系列的校验,通过校验后才执行某些操作。
工作流。企业中通常会制订很多工作流程,一级一级的去解决工作。

上面通过两个案例来学习一下责任链模式。
案例一:创立商品多级校验场景
以创立商品为例,假如商品创立逻辑分为以下三步实现:①创立商品、②校验商品参数、③保留商品。
第②步校验商品又分为多种状况的校验,必填字段校验、规格校验、价格校验、库存校验等等。这些测验逻辑像一个流水线,要想创立出一个商品,必须通过这些校验。如下流程图所示:

伪代码如下:
创立商品步骤,须要通过一系列的参数校验,如果参数校验失败,间接返回失败的后果;通过所有的参数校验后,最终保留商品信息。

如上代码看起来仿佛没什么问题,它十分工整,而且代码逻辑很清晰。

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 配置规定转换成处理器链路对象。代码如下:
/**

  • 应用 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:创立商品所有处理器校验通过,保留商品。
// 创立商品参数,模仿用户传入
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…

正文完
 0