责任链模式的规则树实现——以按规则计算费用为案例

249 阅读9分钟

在实现这个功能的时候,组长千叮咛万嘱咐让我不要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 执行顺序

image.png

RootNode → SwitchRoot → PayNode → BillCycleNode/FormulaNode

0. RootNode作为入口节点,首先将租赁资源和免租信息存入上下文,然后路由到SwitchRoot节点

  1. SwitchRoot节点判断规则是否已生成应收单、是否在生效周期内,符合条件则路由到PayNode节点

  2. PayNode节点根据支付类型(一次性/周期性)路由:

    • 一次性费用(type=0):直接路由到FormulaNode
    • 周期费用(type=1/2):路由到BillCycleNode
  3. BillCycleNode计算计费周期的满月数、剩余天数和额外月度分数,然后路由到FormulaNode

  4. 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 核心设计模式

  1. 责任链模式:规则树节点形成一条责任链,依次执行过滤、路由和计算功能
  2. 策略模式:通过StrategyHandler和StrategyMapper实现不同策略的动态选择
  3. 模板方法模式:AbstractMultiThreadStrategyRouter定义算法骨架,具体节点实现细节
  4. 工厂模式:DefaultStrategyFactory创建DynamicContext,FeeFormulaFactory创建公式服务

4.2 核心组件

  1. 动态上下文(DynamicContext) :贯穿整个规则树执行过程,存储中间结果和共享数据
  2. 策略处理器(StrategyHandler) :处理具体业务逻辑,各节点实现该接口
  3. 策略映射器(StrategyMapper) :决定下一个执行的节点,实现节点间的路由
  4. 抽象路由(AbstractMultiThreadStrategyRouter) :提供异步加载和业务处理的模板方法

4.3 系统特点

  1. 可扩展性:新增规则只需实现新的节点类,通过继承AbstractFeeCalSupport快速集成
  2. 灵活性:通过策略模式和工厂模式,支持不同计费规则和计算公式的动态切换
  3. 可维护性:节点职责单一,代码结构清晰,便于维护和调试
  4. 高性能:支持异步加载数据(multiThread方法),提高系统处理效率

4.4 执行流程概述

  1. 初始化:RootNode接收请求参数,初始化上下文
  2. 过滤:SwitchRoot检查规则有效性和周期
  3. 路由:PayNode根据支付类型选择处理路径
  4. 计算:BillCycleNode计算周期天数,FormulaNode计算费用金额
  5. 返回:封装并返回计算结果