使用责任链模式重构计费
什么是计费?计费是结算系统的核心业务,主要承担各业务系统推送过来的计费需求,是收单流程的重要一环。
业务系统推送业务单据到结算系统以后,结算系统按照计费规则计算各项费用并进行输出,这个过程称为计费。
目前系统主要支持的费用项有货款、物流费、平台佣金、仓储费、推广费、售后费、VAT税、清关费等,本文以货款、物流费、平台佣金为例阐述计费优化的思路和具体实践。
为什么要优化?总结来说,业务上计费科目越来越多,难以快速支持;技术上代码臃肿,可读性差,稳定性不高,难以维护。
计费作为结算系统的基础服务,在系统建立初期就已存在,跟大多数系统一样,早期业务简单,没有必要过度设计,随着系统规模见长,各种需求、逻辑掺杂进来,代码基本都写在一个大类中,很难完全理解业务。并且有些不在计费范围内的业务代码,例如清分和通知上下游,也耦合在计费模块中,业务边界混杂。
计费功能核心流程现状计费流程图计费功能核心代码现状/**
*订单计费服务
*/
publicclassOrderBillingService{
privateLockerlocker;
/**
*计费
*
*@paramorder订单
*@paramevent订单事件
*/
publicvoidbilling(OrderDTOorder,OrderEventevent){
//1.预校验
if(!"ORDER_SIGN".equals(event.getEventType())){
return;
}
//2.并发检查
if(!locker.tryLock(order.getOrderNumber())){
return;
}
//3.幂等检查
BillingOrderbillingOrder=getByOrderNumber(order.getOrderNumber());
if(billingOrder!=null){
return;
}
//4.模型转换
BillingDTObillingDTO=translate(order);
MerchantDTOseller=getSeller(order.getSellerId());
//5.加载计费规则
Contractcontract=loadBillingContract(billingDTO,seller);
Promotionpromotion=loadBillingPromotion(billingDTO,seller);
//6.开始结算计费
BigDecimalorderAmount=billingDTO.getOrderAmount();//订单金额
BigDecimalbillingAmount=BigDecimal.ZERO;
BigDecimallogisticsFeeTotal=billingDTO.getLogisticsFee();//订单物流费
ListBillingSkuDTOorderSkuList=billingDTO.getSkuList();//计费明细
for(BillingSkuDTObillingSkuDTO:orderSkuList){
BigDecimalskuBillingAmount=billingSkuDTO.getAmount();//明细计费金额
//6.1.计算物流费
BigDecimallogisticsFee=billingSkuDTO.getAmount()
.divide(orderAmount,6,BigDecimal.ROUND_HALF_UP)
.multiply(logisticsFeeTotal);//示例按金额占比分摊物流费,实际分摊比这要复杂
FeeDTOlogisticsFeeDTO=newFeeDTO();
logisticsFeeDTO.setFeeType("logistics");
logisticsFeeDTO.setFee(logisticsFee);
logisticsFeeDTO.setAmount(logisticsFee);
billingSkuDTO.addFee(logisticsFeeDTO);
skuBillingAmount=skuBillingAmount.subtract(logisticsFee);
//6.2.计算佣金
BigDecimalcommissionFeeRate=contract.getCommissionFeeRate();
BigDecimalcommissionFee=billingSkuDTO.getAmount().multiply(commissionFeeRate);
FeeDTOcommissionFeeDTO=newFeeDTO();
commissionFeeDTO.setFeeType("commission");
commissionFeeDTO.setFee(commissionFee);
commissionFeeDTO.setAmount(commissionFee);
billingSkuDTO.addFee(commissionFeeDTO);
skuBillingAmount=skuBillingAmount.subtract(commissionFee);
//6.3.计算货款
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
//7.开始优惠计费
for(BillingSkuDTObillingSkuDTO:orderSkuList){
BigDecimalskuBillingAmount=billingSkuDTO.getBillingAmount();//明细计费金额
ListFeeDTOfeeDTOList=billingSkuDTO.getFeeList();
for(FeeDTOfeeDTO:feeDTOList){
//该费用可优惠
if(promotion.isPromotion(feeDTO,billingDTO)){
//获取优惠金额
BigDecimalpromotionFee=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 方法过于复杂,既有校验代码,又有核心计费逻辑,还有计费后的持久化,以及计费职责范围以外的清分、回调等。虽然多数逻辑做了抽离,但是整体逻辑仍然太重,职责很多,相当臃肿,导致计费核心很难扩展。
此次重构的目标就是解决以上问题,让核心逻辑高度内聚,可插拔易扩展,流程可编排、节点可复用,同时逻辑跟其他模块松解耦、不粘连。
重构思路和目标计费流程节点清晰,各个节点业务逻辑比较容易划分边界,所以重构思路上以大类拆分、节点复用为主,比如预校验、模型转换、持久化和清分这几个步骤比较通用,其他各业务计费步骤在逻辑上比较内聚,这两种情况都可以抽象出节点,通过责任链进行编排、重构。
针对上述提到面临的问题,提出以下目标。
易扩展:可扩展性强,流程节点可插拔、可调整,不用大幅改动即可快速支持新需求。
好运维:代码结构清晰,容易理解,出现问题好定位。
使用责任链重构责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
责任链计费类图计费类图定义处理者接口,拆分实现具体的处理者。其中费用计算作为计费整体流程中的一环,自身逻辑内聚,职责单一且又有固定的流程,单独拆分出一个子链出来。
重构后的代码//流程配置
publicclassBillingHandlerChainConfig{
publicBillingHandlerChaingetOrderBillingHandlerChain(){
BillingHandlerChainorderChain=newOrderBillingHandlerChain();
ListBillingHandlerhandlers=newArrayList();
//1.校验
handlers.add(newPreCheckBillingHandler());
//2.模型转换
handlers.add(newTranslateBillingHandler());
//3.计费规则
handlers.add(newRuleBillingHandler());
//4.计费
CalculateBillingHandlercalculateBillingHandler=newCalculateBillingHandler();
handlers.add(calculateBillingHandler);
ListBillingHandlercalculateHandlers=newArrayList();
calculateBillingHandler.setHandlers(calculateHandlers);
//4.1.物流费计算
calculateHandlers.add(newLogisticsBillingHandler());
//4.2.佣金计算
calculateHandlers.add(newCommissionBillingHandler());
//4.3.优惠计算
calculateHandlers.add(newPromotionBillingHandler());
//4.4.货款计算
calculateHandlers.add(newGoodsBillingHandler());
//5.持久化
handlers.add(newSaveBillingHandler());
//6.发布事件
handlers.add(newPublishBillingHandler());
orderChain.setHandlers(handlers);
returnorderChain;
}
}
//计费服务入口
publicclassOrderBillingService{
privateBillingHandlerChainorderBillingHandlerChain;
privatevoidbilling(OrderDTOorder,OrderEventevent){
BillingContextcontext=initContext(order,event);
orderBillingHandlerChain.handle(context);
}
}
//订单计费主链
publicclassOrderBillingHandlerChainimplementsBillingHandlerChain{
privateListBillingHandlerhandlers;
@Override
publicvoidsetHandlers(ListBillingHandlerhandlers){
this.handlers=handlers;
}
@Override
publicbooleanhandle(BillingContextcontext){
if(handlers==null){
thrownewIllegalStateException("订单计费没有可用流程");
}
for(BillingHandlerhandler:handlers){
if(!handler.handle(context)){
returnfalse;
}
}
returntrue;
}
}
//计费处理器/计费子链
publicclassCalculateBillingHandlerimplementsBillingHandler,BillingHandlerChain{
privateListBillingHandlerhandlers;
@Override
publicvoidsetHandlers(ListBillingHandlerhandlers){
this.handlers=handlers;
}
@Override
publicbooleanhandle(BillingContextcontext){
if(handlers==null){
thrownewIllegalStateException("计费子链没有可用流程");
}
for(BillingHandlerhandler:handlers){
if(!handler.handle(context)){
returnfalse;
}
}
returntrue;
}
}
//物流费处理器
publicclassLogisticsBillingHandlerimplementsBillingHandler{
@Override
publicbooleanhandle(BillingContextcontext){
BillingDTObillingDTO=context.getBillingOrder();
BigDecimalorderAmount=billingDTO.getOrderAmount();//订单金额
BigDecimallogisticsFeeTotal=billingDTO.getLogisticsFee();//订单物流费
ListBillingSkuDTOorderSkuList=billingDTO.getSkuList();//计费明细
for(BillingSkuDTObillingSkuDTO:orderSkuList){
BigDecimalskuBillingAmount=billingSkuDTO.getBillingAmount();//明细计费金额
BigDecimallogisticsFee=billingSkuDTO.getAmount()
.divide(orderAmount,6,BigDecimal.ROUND_HALF_UP)
.multiply(logisticsFeeTotal);//示例按金额占比分摊物流费,实际分摊比这要复杂
FeeDTOlogisticsFeeDTO=newFeeDTO();
logisticsFeeDTO.setFeeType("logistics");
logisticsFeeDTO.setFee(logisticsFee);
logisticsFeeDTO.setAmount(logisticsFee);
billingSkuDTO.addFee(logisticsFeeDTO);
skuBillingAmount=skuBillingAmount.subtract(logisticsFee);
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
returntrue;
}
}
//优惠处理器
publicclassPromotionBillingHandlerimplementsBillingHandler{
@Override
publicbooleanhandle(BillingContextcontext){
BillingDTObillingDTO=context.getBillingOrder();
ListBillingSkuDTOorderSkuList=billingDTO.getSkuList();//计费明细
Promotionpromotion=context.getPromotion();
//开始优惠计费
for(BillingSkuDTObillingSkuDTO:orderSkuList){
BigDecimalskuBillingAmount=billingSkuDTO.getBillingAmount();//明细计费金额
ListFeeDTOfeeDTOList=billingSkuDTO.getFeeList();
for(FeeDTOfeeDTO:feeDTOList){
//该费用可优惠
if(promotion.isPromotion(feeDTO,billingDTO)){
//获取优惠金额
BigDecimalpromotionFee=promotion.getPromotionFee(feeDTO,billingDTO);
feeDTO.setPromotion(promotionFee);
feeDTO.setAmount(feeDTO.getAmount().subtract(promotionFee));
skuBillingAmount=skuBillingAmount.add(promotionFee);
}
}
billingSkuDTO.setBillingAmount(skuBillingAmount);
}
returntrue;
}
}
原来逻辑中的清分、计费完成通知上游业务两个环节,本质上不在计费业务的范围内,只是通过计费完成来触发的其他业务,在此次改造中完全独立出去,改由计费的最后一环「发布计费完成事件」来驱动,保持计费业务高内聚。
一些思考腐化产生的原因代码腐化是不可避免的,只能尽量减轻。
腐化产生的原因很多,可能是缺乏良好的设计,一开始就没做好规划,后面业务代码堆砌,就更加混乱,难以维护了。本文所针对的计费模块,就是这个问题。
也可能是缺乏良好的实践,虽然有好的设计和编码标准,但是规约没有执行到位,也是出现腐化的一个重要原因。
当然,这两种原因也都有一些客观因素存在,比如初期业务简单,没必要大搞设计,或者为了需求快速上线,做了一些妥协,成了技术债务。再比如人员更替,技术水平和认知各不相同,后来者也可能不会遵照之前的约定。
如何减轻腐化腐化带来开发效率的降低和系统风险的提升,在实践中可以从下面几点来减轻腐化带来的影响。
制定规约建立包括编码标准,代码审查和单元测试相关的规约,定期总结执行情况,养成防腐的习惯。
定期重构针对已经腐化的代码,定期重构,清理无效代码,去除冗余,改善代码结构,增强可读性,提高可扩展性,降低维护成本。
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线