前言
场景假设:
有一个前台查询业务需要完成以下查询:
- 首先查询商户列表;
- 根据获得商户,查询适配的活动列表 ;
- 根据获得商户,查询商户配置的商品列表;
- 运营区域的定制化推荐标签
- 等等…
同时,不同的页面来源,有不同的查询诉求,例如:运营区H5推广页面,重点突出定制化标签、活动信息、精选商户信息,不展示商品;
场景抽象:
根据场景,我们可以得到如下产品诉求:
- 有很多的查询项需要完成;
- 不同的业务来源,查询项目略有不同;
提示:以下是本篇文章正文可供参考,下文通过多轮方案迭代的形式持续展示演进过程
一、第一轮迭代
技术分析
结合场景假设和场景抽象,我们进行以下技术分析:
- 采用策略模式:以上诸多的查询项,有点技术见识的同学就会得出结论—非常适合策略模式;
- 采用注注解:利用注解提高代码可读性,同时便于实际的策略类生产
- 不同的页面,因为查询诉求不同,前端的入参就会有细微差别
- 需要定义一个查询上下文QueryContext,用来聚合所有的查询诉求,屏蔽差异
- 需要定义一个结果上下文ResultContext,用来存储不同的策略类查询得到的数据
代码示例
定义抽象类,规范基本功能
public abstract class QueryStrategy {
/**
* 执行查询
* @param queryContext 查询上下文
* @param resultContext 结果上下文
*/
public abstract void executeQuery(QueryContext queryContext, ResultContext resultContext);
}
定义注解,标记实际策略类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface QueryRuleType {
/**
* 查询类型
*/
EnumQueryType queryType();
}
定义枚举,标记查询类型
@Getter
@AllArgsConstructor
public enum EnumQueryType {
ACTIVITY("activity","活动查询"),
XXX("","XXX页面");
private String code;
private String desc;
}
实际查询策略类举例:活动查询
@QueryRuleType(queryType = EnumQueryType.ACTIVITY)
public class ActivityQueryStrategy extends QueryStrategy{
public void executeQuery(QueryContext queryContext, ResultContext resultContext){
//实际业务代码
}
}
定义工厂类,实际生产策略类
@Component
public class QueryStrategyFactory{
/**
* 策略注册表
* key:EnumQueryType
* value:QueryStrategy
*/
private final Map<String, QueryStrategy> strategyMap = new HashMap<>();
@Autowired
public void loadEvent(List<QueryStrategy> strategyList){
ListUtils.emptyIfNull(strategyList)
.forEach(
strategy -> {
if (Objects.isNull(strategy)){
return;
}
//获取策略类的注解
QueryRuleType annotation = AnnotationUtils.findAnnotation(strategy.getClass(), QueryRuleType.class);
Optional.ofNullable(
annotation
)
.map(QueryRuleType :: queryType )
.map(EnumQueryType :: code)
.ifPresent(
type -> strategyMap.computeIfAbsent(type,strategy)
);
}
);
}
/**
* 外部直接调用这里,获取策略
*/
public QueryStrategy getStrategy(String type){
return strategyMap.get(type);
}
}
实际业务使用示例
public class OperatingAreaTest {
@Autowired
private QueryStrategyFactory queryStrategyFactory;
public void executeQuery(){
//获取活动查询类
QueryStrategy queryStrategy = queryStrategyFactory.getStrategy(EnumQueryType.ACTIVITY.code);
//获取XX查询类
QueryStrategy queryStrategy = queryStrategyFactory.getStrategy(EnumQueryType.XXX.code);
}
}
二、第二轮迭代
技术分析
基于第一轮迭代,已知我们采用了策略模式,并利用注解完成实际策略类的生产,此时我们又分析得出如下事项:
- 并发查询:诸如:商品列表、活动列表等查询项,两两之间相对独立,可以用并发查询方式,提高查询效率,减少链路耗时
- 基于第1点,我们需要将以上诸多查询项,大致划分为至少两个查询序列(序列也是查询顺序):第一批次查询序列:查询商户列表;第二批次并发查询序列:商品、活动、定制化标签
- 基于第2点,我们要在实际的策略类上标记:查询顺序(序列)
- 我们约定:查询项按照序列分组,依次执行,同一序列,并发执行
同时,需要弥补几个我们忽略的事情,导致一定程度的设计缺陷,引发实际应用策略类时可能会产生业务硬编码:
- 不同的页面,需要的查询项略有不同
- 导致策略工厂类被不同的页面查询类引入用来生产各种实际策略类,不便于管理
- 我们引入的用来屏蔽入参差异的查询上下文的构建分散在不同的页面查询类中,命名和构建方式可以遇见的非常的free style
- 进一步分析后,做出如下改造:
- 抽出公共入参,定义一个BaseRequest类,所有的前台入参必须继承BaseRequest类
- ”每一个页面,一定知道自己需要哪些查询项“,增加模板类,通过增加模板模式限制下游行为
- 提供策略执行方法,统一执行策略,在一定程度上抽象代码,屏蔽硬编码
- 提供子类必须实现的查询上下文构建方法,入参使用父类BaseRequest类
- 查询上下文中增加查询列表注册参数列表
- 进一步分析后,做出如下改造:
代码示例
改造注解,标记实际策略类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface QueryRuleType {
/**
* 查询来源(什么来源)
*/
EnumQuerySourceType querySourceType();
/**
* 查询类型(什么查询)
*/
EnumQueryType queryType();
/**
* 查询序列(什么顺序)
* 约定:按照序列分组;按照顺序执行序列;同一序列,并发执行
*/
Integer queryOrder;
}
增加BaseRequest,收口公共参数
public class BaseRequest {
//定义公共参数
}
查询入参定义示例:运营区域入参
public class OperatingAreaRequest extends BaseRequest {
//定义参数
}
改造查询上下文QueryContext
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class QueryContext {
//其他查询参数
// ...
//注册页面
private EnumQuerySourceType querySourceType;
//注册查询项:查询什么事情(新增)
private List<EnumQueryType> queryTypeList;
}
定义一个策略持有类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StrategyKey {
//哪一个页面
private EnumQuerySourceType querySourceType;
//查询什么
private EnumQueryType queryType;
}
定义一个策略存储类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StrategyValue {
/**
* 查询序列(什么顺序)
* 约定:按照序列分组,依次执行,同一序列,并发执行
*/
Integer queryOrder;
//查询什么事情
private QueryStrategy queryStrategy;
}
改造工厂类,实际生产策略类
@Component
public class QueryStrategyFactory{
/**
* 策略注册表
*/
private final Map<StrategyKey, StrategyValue> strategyMap = new HashMap<>();
@Autowired
public void loadEvent(List<QueryStrategy> strategyList){
ListUtils.emptyIfNull(strategyList)
.forEach(
strategy -> {
if (Objects.isNull(strategy)){
return;
}
//获取策略类的注解
QueryRuleType annotation = AnnotationUtils.findAnnotation(strategy.getClass(), QueryRuleType.class);
//来源
EnumQuerySourceType querySourceType = annotation.querySourceType();
//干什么
EnumQueryType queryType = annotation.queryType();
//顺序
Integer queryOrder = annotation.queryOrder();
//put注册表
strategyMap.computeIfAbsent(
StrategyKey.builder.querySourceType(querySourceType).queryType(queryType).build,
StrategyValue.builder.queryOrder(queryOrder).queryStrategy(strategy).build
);
}
);
}
/**
* 外部直接调用这里,获取策略
*/
public StrategyValue getStrategy(StrategyKey strategyKey){
return strategyMap.get(strategyKey);
}
}
定义基础模板
public abstract class CommonTemplate {
@Autowired
private QueryStrategyFactory queryStrategyFactory;
/**
* 构建查询上下文
* 抽象方法强制子类必须实现
**/
public abstract QueryContext buildQueryContext(BaseRequest baseRequest);
/**
* 执行策略
**/
public final void excuteStrategy(QueryContext queryContext,ResultContext resultContext){
//获取要做哪些事情
List<EnumQueryType> queryTypeList = queryContext.getQueryTypeList();
if (CollectionUtils.isEmpty(queryTypeList)){
throw new RuntimeException("未注册查询项");
}
//来源
EnumQuerySourceType querySourceType = queryContext.getQuerySourceType();
if(Objects.isNull(querySourceType)){
throw new RuntimeException("未注册页面来源");
}
//所有需要执行的策略
List<StrategyValue> strategyValueList = Lists.newArrayList();
queryTypeList.foreach(
queryType->{
StrategyValue queryStrategyValue = queryStrategyFactory.getStrategy(querySourceType,queryType);
if(Objects.isNull(queryStrategyValue)){
return;
}
strategyValueList.add(queryStrategyValue);
}
);
if (CollectionUtils.isEmpty(strategyValueList)){
return;
}
//按照序列分组
Map<Integer ,List<QueryStrategy>> map= strategyValueList.stream.collect(Comparator.groupBy(StrategyValue :: getQueryOrder));
//以下是偷懒的注释,不想继续写了,请注意,翻译!
//拿到map的key
//按顺序for循环
//同一组序列,利用CompletableFuture并发执行
}
}
实际查询模板示例:运营区域查询
public class OperatingAreaTemplate extends CommonTemplate {
/**
* 构建查询上下文
**/
public QueryContext buildQueryContext(BaseRequest baseRequest){
return QueryContext.builder
//其他参数
//...
//其他参数
//注册页面
.querySourceType()
//注册查询项:查询什么事情(新增)
.queryTypeList()
.build;
}
实际业务使用示例
public class OperatingAreaTest {
@Autowired
private OperatingAreaTemplate operatingAreaTemplate;
public void executeQuery(入参传入继承了BaseRequest的参数 request){
//构建查询上下文
QueryContext queryContext = operatingAreaTemplate.buildQueryContext(request);
//构建接受结果的结果上下文
ResultContext resultContext = ResultContext.builder.build;
operatingAreaTemplate.excuteStrategy(queryContext,resultContext);
//后续就可以拿着resultContext 来做其他事情
}
}
三、第三轮迭代
先说题外话
笔者目前从事多年TL工作,在技术方案评审过程中,如果遇到团队内同学对于复杂设计模式应用于实际业务开发的事情,首先会肯定相关同学的设计能力,再和团队一起讨论尝试其他方案,原因如下:
- 团队协作:
- 沟通成本增加很多,甚至需要进行很多次沟通,以确保相关开发人员理解模块设计意图
- 一致性问题爆发,不同的同学对于复杂设计模式的理解不一致,导致后续代码风格、实现方式上出现较大明显偏差
- 对于一个快速发展的团队(笔者一直呆在发展相对较快的部门),对于新人尤其不友好
- 可读性:
- 一大堆和业务不相关的类似源码一般的代码,导致代码晦涩难懂
- 后续持续迭代,技术文档的维护必须及时有效(这个对团队、TL、业务域Owner都有较高要求,说实话,没见过总是及时有效更新文档的)
- 过度设计:
- 其实这项,笔者不是很在乎,因为能设计这种复杂业务流程的同学,其技术功底、业务抽象能力还是值得点赞的;单纯装B的除外
- 过度设计,在技术评审过程中,一定可以识别到并讨论改进方案的
- 测试困难:
- 导致代码调试难度增加,单元测试过程中,可调试性困难,各种对象和方法来回交互
- 集成测试复杂
当然,单纯的使用某一项或者两项设计模式的组合还是可以的,方便表达和系统架构编码,也是日常必须做的事情。
回归正题
我们回顾下上文的叙述,总结下我们面临的业务诉求:
- 有很多的查询项,需要查询
- 查询有先后
- 有并发查询诉求
- 后续还会继续增加查询项
- 随着业务发展
- 随时会新增/筛除查询项
- 随时会调整查询顺序
基于以上总结,我们看到了什么呢,此时终于引出文章标题的最后一段:流程编排
LiteFlow流程引擎
这里笔者先引入LiteFlow官网的一段内容截图:
看完截图,LiteFlow是不是完美的可以作为最终方案选择。
LiteFlow流程引擎
每一个查询项都是一个Node
定义查询Node
@LiteflowComponent("a")
public class ActivityQueryNode extends NodeComponent{
@Override
public void process() {
System.out.println("ActivityQueryNode executed!");
}
}
//定义其他查询Node
@LiteflowComponent("a")
public class XXXQueryNode extends NodeComponent{
@Override
public void process() {
System.out.println("XXXQueryNode executed!");
}
}
流程中断 isEnd
在业务代码中,我们经常遇到需要中断的业务场景,诸如:当商家列表没有查询出来时,不在继续其他查询流程。我们可以使用*isEnd
定义带有中断的查询Node
@LiteflowComponent("a")
public class XXXQueryNode extends NodeComponent{
@Override
public boolean isEnd() {
//判断是否中断
if(){
}
}
@Override
public void process() {
System.out.println("XXXQueryNode executed!");
}
}
LiteFlow规则文件
LiteFlow的规则主要由Node节点和Chain节点组成。
- Chain:代表一个流程,内部需要定义执行哪些节点以及节点的执行规则
- Node:代表一个个查询项
本地规则文件
类似如下官网的xml例子
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(
a, b, WHEN(c,d)
);
</chain>
</flow>
动态规则数据源(笔者推荐)
LiteFlow原生支持了Apollo配置中心,笔者使用了apollo作为规则文件的数据源
需要额外引入插件包
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-apollo</artifactId>
//具体引入的版本自己去官网翻阅,注意和liteflow自身的pom一致
<version>2.12.4.1</version>
</dependency>
再就是告诉liteflow哪里读取文件就可以了
//yaml风格
liteflow:
rule-source-ext-data-map:
chainNamespace: chainConfig
scriptNamespace: scriptConfig
LiteFlow规则执行
//第一个参数为流程ID,第二个参数为流程入参,后面可以传入多个上下文class
public LiteflowResponse execute2Resp(String chainId, Object param, Class<?>... contextBeanClazzArray)
//第一个参数为流程ID,第二个参数为流程入参,后面可以传入多个上下文的Bean
public LiteflowResponse execute2Resp(String chainId, Object param, Object... contextBeanArray)
//获取执行结果
OrderContext orderContext = response.getContextBean(OrderContext.class);
UserContext userContext = response.getContextBean(UserContext.class);
总结
上面写了太多,没什么总结的了,欢迎留言讨论。