小蔡学Java

使用责任链模式重构计费

2023-08-02 14:24 676 0 设计模式 Java设计模式

什么是计费?

计费是结算系统的核心业务,主要承担各业务系统推送过来的计费需求,是收单流程的重要一环。

业务系统推送业务单据到结算系统以后,结算系统按照计费规则计算各项费用并进行输出,这个过程称为计费。

目前系统主要支持的费用项有货款、物流费、平台佣金、仓储费、推广费、售后费、VAT税、清关费等,本文以货款、物流费、平台佣金为例阐述计费优化的思路和具体实践。

为什么要优化?

总结来说,业务上计费科目越来越多,难以快速支持;技术上代码臃肿,可读性差,稳定性不高,难以维护。

计费作为结算系统的基础服务,在系统建立初期就已存在,跟大多数系统一样,早期业务简单,没有必要过度设计,随着系统规模见长,各种需求、逻辑掺杂进来,代码基本都写在一个大类中,很难完全理解业务。并且有些不在计费范围内的业务代码,例如清分和通知上下游,也耦合在计费模块中,业务边界混杂。

计费功能核心流程现状

计费功能核心代码现状

/**
 * 订单计费服务
 */
public class OrderBillingService {
    private Locker locker;

    /**
     * 计费
     *
     * @param order 订单
     * @param event 订单事件
     */
    public void billing(OrderDTO order, OrderEvent event) {
        // 1.预校验
        if (!"ORDER_SIGN".equals(event.getEventType())) {
            return;
        }

        // 2.并发检查
        if (!locker.tryLock(order.getOrderNumber())) {
            return;
        }

        // 3.幂等检查
        BillingOrder billingOrder = getByOrderNumber(order.getOrderNumber());
        if (billingOrder != null) {
            return;
        }

        // 4.模型转换
        BillingDTO billingDTO = translate(order);
        MerchantDTO seller = getSeller(order.getSellerId());

        // 5.加载计费规则
        Contract contract = loadBillingContract(billingDTO, seller);
        Promotion promotion = loadBillingPromotion(billingDTO, seller);

        // 6.开始结算计费
        BigDecimal orderAmount = billingDTO.getOrderAmount(); // 订单金额
        BigDecimal billingAmount = BigDecimal.ZERO;
        BigDecimal logisticsFeeTotal = billingDTO.getLogisticsFee(); // 订单物流费
        List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细

        for (BillingSkuDTO billingSkuDTO : orderSkuList) {
            BigDecimal skuBillingAmount = billingSkuDTO.getAmount(); // 明细计费金额

            // 6.1.计算物流费
            BigDecimal logisticsFee = billingSkuDTO.getAmount()
                    .divide(orderAmount, 6, BigDecimal.ROUND_HALF_UP)
                    .multiply(logisticsFeeTotal); // 示例按金额占比分摊物流费,实际分摊比这要复杂
            FeeDTO logisticsFeeDTO = new FeeDTO();
            logisticsFeeDTO.setFeeType("logistics");
            logisticsFeeDTO.setFee(logisticsFee);
            logisticsFeeDTO.setAmount(logisticsFee);

            billingSkuDTO.addFee(logisticsFeeDTO);
            skuBillingAmount = skuBillingAmount.subtract(logisticsFee);

            // 6.2.计算佣金
            BigDecimal commissionFeeRate = contract.getCommissionFeeRate();
            BigDecimal commissionFee = billingSkuDTO.getAmount().multiply(commissionFeeRate);

            FeeDTO commissionFeeDTO = new FeeDTO();
            commissionFeeDTO.setFeeType("commission");
            commissionFeeDTO.setFee(commissionFee);
            commissionFeeDTO.setAmount(commissionFee);

            billingSkuDTO.addFee(commissionFeeDTO);
            skuBillingAmount = skuBillingAmount.subtract(commissionFee);

            // 6.3.计算货款
            billingSkuDTO.setBillingAmount(skuBillingAmount);
        }

        // 7.开始优惠计费
        for (BillingSkuDTO billingSkuDTO : orderSkuList) {
            BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额
            List<FeeDTO> feeDTOList = billingSkuDTO.getFeeList();

            for (FeeDTO feeDTO : feeDTOList) {
                // 该费用可优惠
                if (promotion.isPromotion(feeDTO, billingDTO)) {
                    // 获取优惠金额
                    BigDecimal promotionFee = promotion.getPromotionFee(feeDTO, billingDTO);

                    feeDTO.setPromotion(promotionFee);
                    feeDTO.setAmount(feeDTO.getAmount().subtract(promotionFee));

                    skuBillingAmount = skuBillingAmount.add(promotionFee);
                }
            }

            // 重新计算货款
            billingSkuDTO.setBillingAmount(skuBillingAmount);

            billingAmount = billingAmount.add(skuBillingAmount);
        }

        billingDTO.setBillingAmount(billingAmount);

        // 8.持久化
        billingOrder = save(billingDTO);

        // 9.清分
        clearing(billingOrder);

        // 10.计费完成
        publish(billingOrder);
    }

以上代码做了很大的简化和提炼,但是仍不难发现其中存在的问题。入口的 billing 方法过于复杂,既有校验代码,又有核心计费逻辑,还有计费后的持久化,以及计费职责范围以外的清分、回调等。虽然多数逻辑做了抽离,但是整体逻辑仍然太重,职责很多,相当臃肿,导致计费核心很难扩展。

此次重构的目标就是解决以上问题,让核心逻辑高度内聚,可插拔易扩展,流程可编排、节点可复用,同时逻辑跟其他模块松解耦、不粘连。

重构思路和目标

计费流程节点清晰,各个节点业务逻辑比较容易划分边界,所以重构思路上以大类拆分、节点复用为主,比如预校验、模型转换、持久化和清分这几个步骤比较通用,其他各业务计费步骤在逻辑上比较内聚,这两种情况都可以抽象出节点,通过责任链进行编排、重构。

针对上述提到面临的问题,提出以下目标。

易扩展:可扩展性强,流程节点可插拔、可调整,不用大幅改动即可快速支持新需求。 好运维:代码结构清晰,容易理解,出现问题好定位。

使用责任链重构

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

计费类图

定义处理者接口,拆分实现具体的处理者。其中费用计算作为计费整体流程中的一环,自身逻辑内聚,职责单一且又有固定的流程,单独拆分出一个子链出来。

重构后的代码

// 流程配置
public class BillingHandlerChainConfig {
    public BillingHandlerChain getOrderBillingHandlerChain() {
        BillingHandlerChain orderChain = new OrderBillingHandlerChain();

        List<BillingHandler> handlers = new ArrayList<>();

        // 1.校验
        handlers.add(new PreCheckBillingHandler());

        // 2.模型转换
        handlers.add(new TranslateBillingHandler());

        // 3.计费规则
        handlers.add(new RuleBillingHandler());

        // 4.计费
        CalculateBillingHandler calculateBillingHandler = new CalculateBillingHandler();
        handlers.add(calculateBillingHandler);
        List<BillingHandler> calculateHandlers = new ArrayList<>();
        calculateBillingHandler.setHandlers(calculateHandlers);
        // 4.1.物流费计算
        calculateHandlers.add(new LogisticsBillingHandler());
        // 4.2.佣金计算
        calculateHandlers.add(new CommissionBillingHandler());
        // 4.3.优惠计算
        calculateHandlers.add(new PromotionBillingHandler());
        // 4.4.货款计算
        calculateHandlers.add(new GoodsBillingHandler());

        // 5.持久化
        handlers.add(new SaveBillingHandler());

        // 6.发布事件
        handlers.add(new PublishBillingHandler());

        orderChain.setHandlers(handlers);

        return orderChain;
    }
}

// 计费服务入口
public class OrderBillingService {
    private BillingHandlerChain orderBillingHandlerChain;

    private void billing(OrderDTO order, OrderEvent event) {
        BillingContext context = initContext(order, event);
        
        orderBillingHandlerChain.handle(context);
    }
}

// 订单计费主链
public class OrderBillingHandlerChain implements BillingHandlerChain {
    private List<BillingHandler> handlers;

    @Override
    public void setHandlers(List<BillingHandler> handlers) {
        this.handlers = handlers;
    }

    @Override
    public boolean handle(BillingContext context) {
        if (handlers == null) {
            throw new IllegalStateException("订单计费没有可用流程");
        }
        for (BillingHandler handler : handlers) {
            if (!handler.handle(context)) {
                return false;
            }
        }
        return true;
    }
}

// 计费处理器/计费子链
public class CalculateBillingHandler implements BillingHandler, BillingHandlerChain {
    private List<BillingHandler> handlers;
    @Override
    public void setHandlers(List<BillingHandler> handlers) {
        this.handlers = handlers;
    }

    @Override
    public boolean handle(BillingContext context) {
        if (handlers == null) {
            throw new IllegalStateException("计费子链没有可用流程");
        }
        for (BillingHandler handler : handlers) {
            if (!handler.handle(context)) {
                return false;
            }
        }
        return true;
    }
}

// 物流费处理器
public class LogisticsBillingHandler implements BillingHandler {

    @Override
    public boolean handle(BillingContext context) {
        BillingDTO billingDTO = context.getBillingOrder();
        BigDecimal orderAmount = billingDTO.getOrderAmount(); // 订单金额
        BigDecimal logisticsFeeTotal = billingDTO.getLogisticsFee(); // 订单物流费
        List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细

        for (BillingSkuDTO billingSkuDTO : orderSkuList) {
            BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额

            BigDecimal logisticsFee = billingSkuDTO.getAmount()
                    .divide(orderAmount, 6, BigDecimal.ROUND_HALF_UP)
                    .multiply(logisticsFeeTotal); // 示例按金额占比分摊物流费,实际分摊比这要复杂
            FeeDTO logisticsFeeDTO = new FeeDTO();
            logisticsFeeDTO.setFeeType("logistics");
            logisticsFeeDTO.setFee(logisticsFee);
            logisticsFeeDTO.setAmount(logisticsFee);

            billingSkuDTO.addFee(logisticsFeeDTO);

            skuBillingAmount = skuBillingAmount.subtract(logisticsFee);

            billingSkuDTO.setBillingAmount(skuBillingAmount);
        }
        return true;
    }
}

// 优惠处理器
public class PromotionBillingHandler implements BillingHandler {

    @Override
    public boolean handle(BillingContext context) {
        BillingDTO billingDTO = context.getBillingOrder();
        List<BillingSkuDTO> orderSkuList = billingDTO.getSkuList(); //计费明细

        Promotion promotion = context.getPromotion();

        // 开始优惠计费
        for (BillingSkuDTO billingSkuDTO : orderSkuList) {
            BigDecimal skuBillingAmount = billingSkuDTO.getBillingAmount(); // 明细计费金额
            List<FeeDTO> feeDTOList = billingSkuDTO.getFeeList();

            for (FeeDTO feeDTO : feeDTOList) {
                // 该费用可优惠
                if (promotion.isPromotion(feeDTO, billingDTO)) {
                    // 获取优惠金额
                    BigDecimal promotionFee = promotion.getPromotionFee(feeDTO, billingDTO);

                    feeDTO.setPromotion(promotionFee);
                    feeDTO.setAmount(feeDTO.getAmount().subtract(promotionFee));

                    skuBillingAmount = skuBillingAmount.add(promotionFee);
                }
            }

            billingSkuDTO.setBillingAmount(skuBillingAmount);
        }
        return true;
    }
}

原来逻辑中的清分、计费完成通知上游业务两个环节,本质上不在计费业务的范围内,只是通过计费完成来触发的其他业务,在此次改造中完全独立出去,改由计费的最后一环「发布计费完成事件」来驱动,保持计费业务高内聚。

一些思考

腐化产生的原因

代码腐化是不可避免的,只能尽量减轻。

腐化产生的原因很多,可能是缺乏良好的设计,一开始就没做好规划,后面业务代码堆砌,就更加混乱,难以维护了。本文所针对的计费模块,就是这个问题。

也可能是缺乏良好的实践,虽然有好的设计和编码标准,但是规约没有执行到位,也是出现腐化的一个重要原因。 当然,这两种原因也都有一些客观因素存在,比如初期业务简单,没必要大搞设计,或者为了需求快速上线,做了一些妥协,成了技术债务。再比如人员更替,技术水平和认知各不相同,后来者也可能不会遵照之前的约定。

如何减轻腐化

腐化带来开发效率的降低和系统风险的提升,在实践中可以从下面几点来减轻腐化带来的影响。

制定规约

建立包括编码标准,代码审查和单元测试相关的规约,定期总结执行情况,养成防腐的习惯。

定期重构

针对已经腐化的代码,定期重构,清理无效代码,去除冗余,改善代码结构,增强可读性,提高可扩展性,降低维护成本。

评论( 0 )

  • 博主 Mr Cai
  • 坐标 河南 信阳
  • 标签 Java、SpringBoot、消息中间件、Web、Code爱好者

文章目录