在实现这个功能的时候,组长千叮咛万嘱咐让我不要if到底,不然会巨难维护,但是根据合同的规则进行费用计算,需要考虑规则的不同情况,我初步计算了一下,如果仅采用策略模式加工厂模式,排列组合下来我得写二十多个策略类,瞬间人傻,然后发现了规则树这样的思路,将职责分开,可以有效提高代码复用性和维护性,因此使用规则树配合策略模式、工厂模式、模板模式完成了该功能的开发,接下来进行介绍。(着重介绍思路)
思路参考:【《重学Java设计模式》扩展篇;链式设计方案,1个责任链、2个规则树】 www.bilibili.com/video/BV1oY…
0 费用计算完整流程
开始
│
├─ 初始化上下文(RootNode)
│
├─ 前置检查(SwitchRoot)
│ ├─ 检查是否已生成应收单
│ ├─ 过滤不在合同期的业务
│ └─ 匹配计费周期
│
├─ 支付类型路由(PayNode)
│ ├─ 一次性费用 → 直接计算
│ └─ 周期费用 → 周期计算节点
│
├─ 周期计算(BillCycleNode)
│ ├─ 计算满月数和剩余天数
│ ├─ 计算月份分数
│ └─ 存储计算结果
│
└─ 费用计算(FormulaNode)
├─ 根据规则类型选择计费公式
├─ 计算基础金额
├─ 处理免租期
├─ 计算最终金额
└─ 返回计算结果
一、整体规则树设计
1.1 核心接口设计
StrategyHandler接口
public interface StrategyHandler<T, D, R> {
StrategyHandler DEFAULT = (T, D) -> null;
R apply(T requestParameter, D dynamicContext) throws Exception;
}
该接口定义了策略处理器,包含一个apply方法用于处理业务逻辑,泛型参数T表示请求参数类型,D表示动态上下文类型,R表示返回结果类型。
StrategyMapper接口
public interface StrategyMapper<T, D, R> {
StrategyHandler<T, D, R> get(T requestParameter, D dynamicContext) throws Exception;
}
该接口定义了策略映射器,用于根据请求参数和上下文获取对应的策略处理器。
1.2 抽象类设计
AbstractMultiThreadStrategyRouter抽象类
public abstract class AbstractMultiThreadStrategyRouter<T, D, R> implements StrategyMapper<T, D, R>, StrategyHandler<T, D, R> {
protected StrategyHandler<T, D, R> defaultStrategyHandler = StrategyHandler.DEFAULT;
public R router(T requestParameter, D dynamicContext) throws Exception {
StrategyHandler<T, D, R> strategyHandler = get(requestParameter, dynamicContext);
if (null != strategyHandler) return strategyHandler.apply(requestParameter, dynamicContext);
return defaultStrategyHandler.apply(requestParameter, dynamicContext);
}
@Override
public R apply(T requestParameter, D dynamicContext) throws Exception {
multiThread(requestParameter, dynamicContext);
return doApply(requestParameter, dynamicContext);
}
protected abstract void multiThread(T requestParameter, D dynamicContext);
protected abstract R doApply(T requestParameter, D dynamicContext) throws Exception;
}
该抽象类实现了StrategyMapper和StrategyHandler接口,采用了模板方法设计模式,定义了算法的骨架:
- router方法:根据请求参数和上下文获取策略处理器并执行
- apply方法:先执行异步加载(multiThread),再执行业务处理(doApply)
- 抽象方法multiThread:用于异步加载数据(本功能实现没有用到)
- 抽象方法doApply:用于具体业务逻辑处理
AbstractFeeCalSupport抽象类
public abstract class AbstractFeeCalSupport extends AbstractMultiThreadStrategyRouter<FeeStrategyParam, DefaultStrategyFactory.DynamicContext, FeeStrategyResult> {
@Override
protected void multiThread(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) {
// 缺省的方法
}
}
该类继承自AbstractMultiThreadStrategyRouter,是所有计费节点的基类,泛型参数指定为FeeStrategyParam(请求参数)、DynamicContext(动态上下文)和FeeStrategyResult(结果)。
1.3 新增节点示例
新增一个自定义节点需继承AbstractFeeCalSupport并实现抽象方法:
@Component
public class CustomNode extends AbstractFeeCalSupport {
@Autowired
private NextNode nextNode;
@Override
protected FeeStrategyResult doApply(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 业务逻辑处理
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<FeeStrategyParam, DefaultStrategyFactory.DynamicContext, FeeStrategyResult> get(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 根据条件返回下一个节点
return nextNode;
}
}
二、现有节点结构与执行顺序
2.1 节点结构
规则树包含以下核心节点,形成一条责任链:
- RootNode:根节点,规则树的入口
- SwitchRoot:开关节点,负责规则过滤和路由
- PayNode:支付周期节点,根据支付类型路由
- BillCycleNode:计费周期节点,计算计费周期和天数
- FormulaNode:计算公式节点,计算费用金额
2.2 执行顺序
RootNode → SwitchRoot → PayNode → BillCycleNode/FormulaNode
0. RootNode作为入口节点,首先将租赁资源和免租信息存入上下文,然后路由到SwitchRoot节点
-
SwitchRoot节点判断规则是否已生成应收单、是否在生效周期内,符合条件则路由到PayNode节点
-
PayNode节点根据支付类型(一次性/周期性)路由:
- 一次性费用(type=0):直接路由到FormulaNode
- 周期费用(type=1/2):路由到BillCycleNode
-
BillCycleNode计算计费周期的满月数、剩余天数和额外月度分数,然后路由到FormulaNode
-
FormulaNode使用FeeFormulaFactory获取相应的计算公式,计算费用并返回结果
三、节点任务与设计模式的体现
3.1 RootNode
任务:
- 初始化上下文,设置租赁资源和免租信息
- 作为规则树的入口点,路由到SwitchRoot节点
设计模式:
- 责任链模式:作为规则树的第一个节点,启动责任链执行
- 模板方法模式:继承AbstractFeeCalSupport,实现doApply和get方法
┌────────────────────────────┐
│ 🚪 根节点入口 │
│ RootNode │
└─────────────┬──────────────┘
↓
写入租赁资源与免租信息至上下文
↓
跳转至 SwitchRoot
@Slf4j
@Component
public class RootNode extends AbstractFeeCalSupport {
@Autowired
private SwitchRoot switchRoot;
@Override
protected FeeStrategyResult doApply(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
dynamicContext.set免租期
dynamicContext.set租赁资源等等信息
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<FeeStrategyParam, DefaultStrategyFactory.DynamicContext, FeeStrategyResult> get(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
return switchRoot; //路由到开关节点
}
}
3.2 SwitchRoot
任务:
- 判断规则是否已生成应收单,避免重复生成
- 验证当前业务月是否在规则生效周期内
- 加载计费周期信息到上下文
- 符合条件则路由到PayNode节点
设计模式:
- 责任链模式:作为规则树的第二个节点,进行规则过滤
- 策略模式:根据规则类型和业务日期判断是否继续执行,将每种情况都写成策略类,最后结合工厂模式进行装配和使用
- 模板方法模式:实现doApply和get方法
关键逻辑:
┌───────────────┐
│ 入口方法 │
└────┬──────────┘
↓
判断是否已生成应收单?
│ 是 ↓ 否
│ 是否为一次性规则?
↓ 是 ↓ 否
是否为合同起始月? 判断是否为周期规则?
↓ 否 ↓ 是 ↓ 否 ↓ 是
return 进入payNode return 判断当前业务月是否命中周期
↓ 否 ↓ 是
return 进入payNode
@Override
protected FeeStrategyResult doApply(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
log.info("【开关节点SwitchRoot】规则决策树 月份:{}", requestParameter);
return router(requestParameter, dynamicContext);
}
@Override
public StrategyHandler<FeeStrategyParam, DefaultStrategyFactory.DynamicContext, FeeStrategyResult> get(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) throws Exception {
// 开关节点负责判断该规则是否应该生效,因此这里要判断的很多,只列举重要的判断逻辑
// 在这里查询应收单数据库,判断当前合同当前业务月当前规则是否已经生成,如果生成直接返回,否则继续执行下面的规则
if (当前业务月当前规则是否已经生成) {
log.info("【开关节点SwitchRoot】规则决策树 月份:{} 该规则已经生成,直接返回");
return defaultStrategyHandler;
}
//
if (如果是一次性规则,并且当前业务月不是合同所在月)) {
log.info("【开关节点SwitchRoot】规则决策树 月份:{} 一次性规则不在当前所在月生效,直接返回”);
return defaultStrategyHandler;
}
// 如果是周期性规则,且当前业务月不在计费周期内,则直接返回
if (周期性规则) {
//判断当前业务月是否在计费周期内
if (!isBillingMonth(businessDate, rule, dynamicContext)) {
log.info("【开关节点SwitchRoot】规则决策树 月份:{} 周期性规则不在当前所在月生效,直接返回");
return defaultStrategyHandler;
}
}
return payNode;
}
3.3 PayNode
任务:
- 根据支付周期类型路由到不同节点
- 一次性费用:直接进入FormulaNode计算费用
- 周期费用:进入BillCycleNode计算周期天数
设计模式:
- 责任链模式:作为规则树的第三个节点,进行支付类型路由
- 策略模式:根据支付类型选择不同的后续节点
- 模板方法模式:实现doApply和get方法
┌────────────────────┐
│ 进入支付周期节点 │
└────────┬───────────┘
↓
判断规则类型是一次性还是周期性?
│ 一次性 ↓ 周期性
│ 进入计费周期判断节点(BillCycleNode)
↓
进入公式计算节点(FormulaNode)
@Override
public StrategyHandler get(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) {
if (rule == 一次性费用) {
return formulaNode; // 一次性费用
}
return billCycleNode; // 周期费用
}
3.4 BillCycleNode
任务:
- 计算每个周期的满月数、剩余天数和额外月度分数
- 支持三种计算方式:自然月、30天/月、30.5天/月
- 将计算结果(实际计费天数与周期的映射)存入上下文,路由到FormulaNode
设计模式:
- 责任链模式:作为规则树的第四个节点,处理周期计算
- 模板方法模式:实现doApply和get方法
- 策略模式:根据calculatedMonthType选择不同的天数计算策略
┌────────────────────────────┐
│ 进入计费周期节点 │
└─────────────┬──────────────┘
↓
获取匹配的计费周期列表
↓
是否存在匹配的计费周期?
↓ 否 ↓ 是
返回空结果 遍历每个匹配周期
↓
计算该周期内的满月数和剩余天数
↓
将周期及计算结果保存至上下文
↓
处理下一个匹配周期(循环)
↓
所有周期计算完成后
↓
路由到下一个处理节点(公式计算节点)
3.5 FormulaNode
任务:
- 使用FeeFormulaFactory获取相应的计算公式
- 调用公式服务计算费用
- 封装并返回计算结果
设计模式:
- 责任链模式:作为规则树的最后一个节点,计算最终费用
- 工厂模式:通过FeeFormulaFactory获取具体的公式服务
- 策略模式:不同的费用规则对应不同的计算公式
┌────────────────────────────┐
│ 计费公式计算节点 │
│ FormulaNode │
└─────────────┬──────────────┘
↓
获取当前计费规则 → 找到对应公式策略
↓
执行公式进行金额计算
↓
封装计算结果与周期信息 → 返回最终结果 ✅
@Override
protected FeeStrategyResult doApply(FeeStrategyParam requestParameter, DefaultStrategyFactory.DynamicContext dynamicContext) {
FeeRule rule = requestParameter.getRule();
IFormulaService formulaService = feeFormulaFactory.getFormulaService(rule);
FormulaResult formulaResult = formulaService.calculateFee(rule, dynamicContext);
// 封装结果...
return result;
}
四、整体系统总结
4.1 核心设计模式
- 责任链模式:规则树节点形成一条责任链,依次执行过滤、路由和计算功能
- 策略模式:通过StrategyHandler和StrategyMapper实现不同策略的动态选择
- 模板方法模式:AbstractMultiThreadStrategyRouter定义算法骨架,具体节点实现细节
- 工厂模式:DefaultStrategyFactory创建DynamicContext,FeeFormulaFactory创建公式服务
4.2 核心组件
- 动态上下文(DynamicContext) :贯穿整个规则树执行过程,存储中间结果和共享数据
- 策略处理器(StrategyHandler) :处理具体业务逻辑,各节点实现该接口
- 策略映射器(StrategyMapper) :决定下一个执行的节点,实现节点间的路由
- 抽象路由(AbstractMultiThreadStrategyRouter) :提供异步加载和业务处理的模板方法
4.3 系统特点
- 可扩展性:新增规则只需实现新的节点类,通过继承AbstractFeeCalSupport快速集成
- 灵活性:通过策略模式和工厂模式,支持不同计费规则和计算公式的动态切换
- 可维护性:节点职责单一,代码结构清晰,便于维护和调试
- 高性能:支持异步加载数据(multiThread方法),提高系统处理效率
4.4 执行流程概述
- 初始化:RootNode接收请求参数,初始化上下文
- 过滤:SwitchRoot检查规则有效性和周期
- 路由:PayNode根据支付类型选择处理路径
- 计算:BillCycleNode计算周期天数,FormulaNode计算费用金额
- 返回:封装并返回计算结果