返回

链式多分支规则树模型

前言

最新在学习项目的时候发现了一个很巧妙的设计模式,虽然长期处于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方法。这里较为巧妙的是使用了抽象类来继承,这样既可以有自己的实现方法,也不需要在当前这个类中实现 getapply方法。

场景

现在将上述的处理器、映射器、路由器结合到拼团的实际场景中来。先创建销售商品信息以及试算结果实体,如下。

当然,这里只是为了在整个流程的实现过程中不会报错,实际上可能用不上这几个类。

/**
 * 销售商品信息,也就是业务节点的入参
 */
@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;
    }
}

到这里,也就完成了链式多分支规则树模型的线程池引入。

你要相信流星划过会带给我们幸运,就像现实告诉你我要心存感激