前言
最新在学习项目的时候发现了一个很巧妙的设计模式,虽然长期处于MVC架构的桎梏下,遇见这种设计模式难免令人眼前一亮,遂打算记录一下。
背景
项目背景是在一个拼团活动的场景下。对于这么一个业务,当点击进入商品页面时,如果商品存在优惠信息我们总是能够看到优惠之后的价格,这也就是商品的试算。这时候不妨想想,如果有多种优惠活动,会产生不同的优惠金额,那么要怎么实现这种逻辑呢?在传统的MVC架构下,最常见的可能就是将不同的优惠活动使用不同的Service进行封装,在商品试算的时候使用if else来决定采用哪种优惠策略。但这显然是不够优雅的,如果是在DDD架构下,设计模式能够摆脱MVC的限制,从而更好的融合到项目中,这也就是这篇博客要阐述的内容,即”链式多分支规则树模型“
碍于笔者的技术所限,MVC架构应当也有一些其他的方式能够避免上述if else的情况,这里权且就不再深究
链式多分支规则树模型
先整体看一下模型的结构,如下图所示
- 首先,定义抽象的通用的规则树模型结构。涵盖;StrategyMapper - 策略映射器、StrategyHandler - 策略处理器、
AbstractStrategyRouter<T, D, R>
- 策略路由抽象类。通过泛型设计允许使用方可以自定义出入参和动态上下文,让抽象模板模型具有通用性。 - 之后,由使用方自定义出工厂、功能抽象类和一个个流程流转的节点。这些节点可以自由组装进行流转,相比于责任链它的实现方式更具有灵活性。
策略处理器
策略处理器的接口代码如下所示:
public interface StrategyHandler<T, D, R> {
/** 默认处理 */
/// 函数式接口,因此DEFAULT等效于一个始终返回null的apply方法的有效实现
StrategyHandler DEFAULT = (T, D) -> null;
/**
* 处理业务流程
* @param requestParam 入参
* @param dynamicContext 上下文
* @return 返回参数
* @throws Exception
*/
R apply(T requestParam, D dynamicContext) throws Exception;
}
策略处理器用来处理执行的业务流程。在每个业务流程执行时,如果有某些数据是前面的节点到后面的节点都会使用到的,那么就可以将这些数据存放到上下文中。同时,实现一个默认的处理器,对所有的入参都返回空。可以用来停止流程。
策略映射器
策略映射器的接口代码如下:
public interface StrategyMapper<T, D, R> {
/**
* 获取对应的策略处理器
* @param requestParam 入参
* @param dynamicContext 上下文
* @return 返回参数
* @throws Exception
*/
StrategyHandler<T, D, R> get(T requestParam, D dynamicContext) throws Exception;
}
策略映射器提供了 get
方法来获取策略处理器,也就相当于获取业务流程的每个节点,这也就避免了将所有逻辑都写到一个类中。
策略路由器
处理器和映射器分别用来处理业务流程和获取业务流程的每个节点,那么真正决定业务流程的走向便是路由器。其具体实现如下:
/**
* 路由器实现两个映射器和处理其两个接口
* @param <T>
* @param <D>
* @param <R>
*/
public abstract class AbstractStrategyRouter<T, D, R> implements StrategyHandler<T, D, R>, StrategyMapper<T, D, R> {
@Getter
@Setter
protected StrategyHandler<T, D, R> defaultHandler = StrategyHandler.DEFAULT;
public R router(T requestParam, D dynamicContext) throws Exception {
StrategyHandler<T, D, R> handler = get(requestParam, dynamicContext);
if (null != handler) {
return handler.apply(requestParam, dynamicContext);
}
return defaultHandler.apply(requestParam, dynamicContext);
}
}
AbstractStrategyRouter实现了映射器和处理器两个接口,这样就能够在本类中实现的 router
方法内使用映射器和处理器中的 get
方法和 apply
方法。这里较为巧妙的是使用了抽象类来继承,这样既可以有自己的实现方法,也不需要在当前这个类中实现 get
和 apply
方法。
场景
现在将上述的处理器、映射器、路由器结合到拼团的实际场景中来。先创建销售商品信息以及试算结果实体,如下。
当然,这里只是为了在整个流程的实现过程中不会报错,实际上可能用不上这几个类。
/**
* 销售商品信息,也就是业务节点的入参
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MarketProductEntity {
/** 活动ID */
private Long activityId;
/** 用户ID */
private String userId;
/** 商品ID */
private String goodsId;
}
/**
* 试算结果实体类,也就是业务节点的返参
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrialBalanceEntity {
/** 商品ID */
private String goodsId;
/** 商品名称 */
private String goodsName;
/** 原始价格 */
private BigDecimal originalPrice;
/** 折扣价格 */
private BigDecimal deductionPrice;
}
接下来创建功能服务支撑类,以便之后的业务流程节点继承。这里继续使用抽象类即可,因为方法的最终实现还是要交给业务流程节点来实现,不必在服务支撑类中实现,同时,到了服务支撑类这一层,就可以将之前准备的入参和返回参数替代接口中的泛型了。到这里还存在一些问题,之前提到的上下文要怎么存放呢?最开始的节点要怎么进入并使用?这些可以通过创建另一个类来同时管理,这里命名为 DefaultActivityStrategyFactory
,于是,服务支撑类的代码以及 DefaultActivityStrategyFactory
代码如下:
/**
* 服务支撑类,继承原来的策略路由器
* @param <MarketProductEntity> 入参
* @param <DynamicContext> 动态上下文
* @param <TrialBalanceEntity> 返参
*/
public abstract class AbstractGroupMarketSupport<MarketProductEntity, DynamicContext, TrialBalanceEntity>
extends AbstractStrategyRouter<MarketProductEntity, DynamicContext, TrialBalanceEntity> {
}
/**
* 一个工厂类,用来管理初始流程节点和动态上下文
*/
@Service
public class DefaultActivityStrategyFactory {
/** 业务流程中的起始节点 */
private final RootNode rootNode;
public DefaultActivityStrategyFactory(RootNode rootNode) {
this.rootNode = rootNode;
}
public StrategyHandler<MarketProductEntity, DynamicContext, TrialBalanceEntity> strategyHandler() {
return rootNode;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class DynamicContext {
/** 保存上下文信息 */
}
}
如上面的代码,服务支撑类继承了原先的策略路由器,使得其同时拥有了映射器和处理器的方法,而使用一个工厂类来管理动态上下文和初始节点, 随着业务的不断扩展,流程可能需要更多的信息,这时候就可以往动态上下文中添加信息来满足流程的执行。
最后就是创建每个节点,节点的创建方式是一样的,唯一有区别的可能就是在业务逻辑上。四个节点分别如下:
RootNode
@Slf4j
@Service
public class RootNode extends AbstractGroupMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private SwitchNode switchNode;
@Override
public TrialBalanceEntity apply(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("==== RootNode 开始执行 ====");
dynamicContext.setInfo("### 这里是业务流程要使用的信息");
log.info("==== 在RootNode中存入上下文信息 信息为:{}", dynamicContext.getInfo());
return router(requestParam, dynamicContext);
}
/**
* 获得起始节点的下一个节点
* @param requestParam 入参
* @param dynamicContext 上下文
* @return 返参
* @throws Exception
*/
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return switchNode;
}
}
SwitchNode
@Slf4j
@Service
public class SwitchNode extends AbstractGroupMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private MarketNode marketNode;
@Override
public TrialBalanceEntity apply(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("==== SwitchNode 开始执行 ====");
return router(requestParam, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return marketNode;
}
}
MarketNode
@Slf4j
@Service
public class MarketNode extends AbstractGroupMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private EndNode endNode;
@Override
public TrialBalanceEntity apply(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("==== MarketNode 开始执行 ====");
// 可以在这里执行查询商品和活动信息并试算,同时利用上下文信息
log.info("==== 取出上下文信息,信息为:{}", dynamicContext.getInfo());
return router(requestParam, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return endNode;
}
}
EndNode
@Slf4j
@Service
public class EndNode extends AbstractGroupMarketSupport<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Override
public TrialBalanceEntity apply(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("==== EndNode 开始执行 ====");
/* 此处就可以返回试算结果了 */
return new TrialBalanceEntity();
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return defaultHandler;
}
}
最后,用单元测试来验证一下执行的顺序,单元测试类如下:
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class IndexTest {
@Resource
private DefaultActivityStrategyFactory defaultActivityStrategyFactory;
@Test
public void test() throws Exception {
StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> handler = defaultActivityStrategyFactory.strategyHandler();
TrialBalanceEntity trialBalance = handler.apply(new MarketProductEntity(), new DefaultActivityStrategyFactory.DynamicContext());
}
}
得到的执行结果为:
可以看到,每个节点按照既定的顺序执行,并可以在流程中使用上下文信息。
优化
通过上述对链式多分支规则树模型的讲述,已经有了较为详细的了解。下面,针对实际可能出现的业务场景,我们引入线程池来优化程序的执行时间。
背景
在规则树模型结构中,如果要进行商品的试算,至少需要查询商品信息和活动信息才有可能计算商品的折扣。如果这个步骤放在主线程中执行,势必会拖慢主线程的执行。如果查询的数据很多,那么这个代价肯定是不可接受的。所以,这时候可以考虑引入线程池,将必要的查询交由线程池中的线程实现,尽可能的减少对主线程的影响。
线程池创建
创建线程池的方式有很多种,这里也是准备了一种较为优雅的方式。首先在yml配置文件中先配置需要用到的配置信息。
# 线程池配置
thread:
pool:
executor:
config:
core-pool-size: 20
max-pool-size: 50
keep-alive-time: 5000
block-queue-size: 5000
policy: CallerRunsPolicy
接下来我们创建一个线程池配置属性类( ThreadPoolConfigProperties
)来接收yml文件中的配置信息。
@Data
@ConfigurationProperties(prefix = "thread.pool.executor.config", ignoreInvalidFields = true)
public class ThreadPoolConfigProperties {
/** 核心线程数 */
private Integer corePoolSize;
/** 最大线程数 */
private Integer maxPoolSize;
/** 最大等待时间 */
private Long keepAliveTime;
/** 最大队列数 */
private Integer blockQueueSize;
/*
* AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
* DiscardPolicy:直接丢弃任务,但是不会抛出异常
* DiscardOldestPolicy:将最早进入队列的任务删除,之后再尝试加入队列的任务被拒绝
* CallerRunsPolicy:如果任务添加线程池失败,那么主线程自己执行该任务
* */
private String policy;
}
这里需要使用注解 @ConfigurationPorperties
来指定在yml文件中的配置信息, prefix
也就是yml配置中的前缀信息。最后就是创建真正的线程池配置类( ThreadPoolConfig
)。
/**
* 线程池配置信息
* @EnableAsync 这个注解允许方法在后台线程中异步执行,而不是阻塞调用线程
*/
@Slf4j
@EnableAsync
@Configuration
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
public class ThreadPoolConfig {
@Bean
@ConditionalOnMissingBean(ThreadPoolExecutor.class) // 这个注解用来保证线程池的唯一性,当不存在线程池时才会创建
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties properties) {
// 实例化策略,用于之后实际创建用
RejectedExecutionHandler handler;
switch (properties.getPolicy()) {
case "AbortPolicy":
handler = new ThreadPoolExecutor.AbortPolicy();
break;
case "DiscardPolicy":
handler = new ThreadPoolExecutor.DiscardPolicy();
break;
case "DiscardOldestPolicy":
handler = new ThreadPoolExecutor.DiscardOldestPolicy();
break;
case "CallerRunsPolicy":
handler = new ThreadPoolExecutor.CallerRunsPolicy();
break;
default:
handler = new ThreadPoolExecutor.AbortPolicy();
break;
}
return new ThreadPoolExecutor(properties.getCorePoolSize(),
properties.getMaxPoolSize(),
properties.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(properties.getBlockQueueSize()),
Executors.defaultThreadFactory(),
handler);
}
}
其中一些比较关键的信息通过注释呈现。
改造链式多分支规则树模型
回想一下,每个流程节点其实都是继承自 AbstractGroupMarketSupport
类,而这个类又是继承自 AbstractStrategyRouter
这个策略路由器。所以从最上层开始,引入需要的线程池方法。
我们重新创建一个类 AbstractMutiThreadStrategyRouter
代替原先的 AbstractStrategyRouter
。
public abstract class AbstractMutiThreadStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {
@Getter
@Setter
protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;
public R router(T requestParam, D dynamicContext) throws Exception {
StrategyHandler<T, D, R> strategyHandler = get(requestParam, dynamicContext);
if (null != strategyHandler) {
return strategyHandler.apply(requestParam, dynamicContext);
}
return defaultStrategyHandler.apply(requestParam, dynamicContext);
}
@Override
public R apply(T requestParam, D dynamicContext) throws Exception {
mutiThread(requestParam, dynamicContext);
return doApply(requestParam, dynamicContext);
}
protected abstract R doApply(T requestParam, D dynamicContext) throws Exception;
protected abstract void mutiThread(T requestParam, D dynamicContext) throws Exception;
}
与原先不同的是,在这个策略路由器中,对 apply
方法进行了简单的实现,在内部执行两个方法:
mutiThread
方法,也就是线程池查找的相应方法doApply
方法,真正的业务逻辑实现
值得注意的是,这两个方法依旧为抽象方法,具体的实现交由子类负责。
在服务支持类上没有过多的改动,但需要实现缺省的 mutiThread
方法,这样可以避免之后的每个业务流程节点都要实现这一方法。
public abstract class AbstractGroupBuyMarketSupport<MarketProductEntity, DynamicContext, TrialBalanceEntity>
extends AbstractMutiThreadStrategyRouter<MarketProductEntity, DynamicContext, TrialBalanceEntity> {
/** 在这里注入查询接口,当流程节点使用时可以不用再次注入 */
@Resource
protected IActivityRepository repository;
@Override
protected void mutiThread(MarketProductEntity requestParam, DynamicContext dynamicContext) throws Exception {
// 缺省的方法,避免每个子类都要实现
}
}
最后就是业务流程节点的实现,每个业务流程节点只需要实现自己对应的业务逻辑,也就是 doApply
方法,在需要查询的时候额外实现 mutiThread
方法即可,这里使用MarketNode作为例子。假如我们要在MarketNode执行时获得商品信息,可以这么做。
首先需要创建交由线程池执行的线程,再因为查询需要返回结果,所以使用 FutureTask的方式来创建线程。如下:
public class QuerySkuVOThreadTask implements Callable<SkuVO> {
/** 商品ID */
private final String goodsId;
/** 查询接口 */
private final IActivityRepository repository;
public QuerySkuVOThreadTask(String goodsId, IActivityRepository repository) {
this.goodsId = goodsId;
this.repository = repository;
}
@Override
public SkuVO call() throws Exception {
return repository.querySkuVO(goodsId);
}
}
最后只需要在MarketNode节点中实现 mutiThread
方法,并将查询任务交给线程池执行即可。
@Slf4j
@Service
public class MarketNode extends AbstractGroupBuyMarketSupport
<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> {
@Resource
private ThreadPoolExecutor threadPoolExecutor;
@Resource
private EndNode endNode;
/**
* 实现异步数据加载,并存储到动态上下文中
* @param requestParam
* @param dynamicContext
* @throws Exception
*/
@Override
protected void mutiThread(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 通过线程池去查询商品信息
QuerySkuVOFromDBThreadTask querySkuVOFromDBThreadTask = new QuerySkuVOFromDBThreadTask(requestParam.getGoodsId(), repository);
FutureTask<SkuVO> skuVOFutureTask = new FutureTask<>(querySkuVOFromDBThreadTask);
threadPoolExecutor.execute(skuVOFutureTask);
}
@Override
public TrialBalanceEntity doApply(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) throws Exception {
return router(requestParam, dynamicContext);
}
@Override
public StrategyHandler<MarketProductEntity, DefaultActivityStrategyFactory.DynamicContext, TrialBalanceEntity> get(MarketProductEntity requestParam, DefaultActivityStrategyFactory.DynamicContext dynamicContext) {
return endNode;
}
}
到这里,也就完成了链式多分支规则树模型的线程池引入。