过程重构

过程重构

以典型的电商系统为例,其往往某个操作会包含复杂的逻辑过程:

其中某个上架的链路,可能包含以下的流程:

面对这种复杂的逻辑,我们首先想到的就是分治理念,譬如引入流程引擎等专门的 Pipeline 处理流程。不过在实践中,如 KISS(Keep It Simple and Stupid)原则所示,最好是什么工具都不要用,次之是用一个极简的 Pipeline 模式,最差是使用像流程引擎这样的重方法。除非你的应用有极强的流程可视化和编排的诉求,否则非常不推荐使用流程引擎等工具。第一,它会引入额外的复杂度,特别是那些需要持久化状态的流程引擎;第二,它会割裂代码,导致阅读代码的不顺畅。

分解与抽象

回到商品上架的问题,这里问题核心是工具吗?是设计模式带来的代码灵活性吗?显然不是,问题的核心应该是如何分解问题和抽象问题,知道金字塔原理的应该知道,此处,我们可以使用结构化分解将问题解构成一个有层级的金字塔结构:

以商品上架为例,程序的入口是一个上架命令(OnSaleCommand), 它由三个阶段(Phase)组成。

@Command
public class OnSaleNormalItemCmdExe {
@Resource
private OnSaleContextInitPhase onSaleContextInitPhase;
@Resource
private OnSaleDataCheckPhase onSaleDataCheckPhase;
@Resource
private OnSaleProcessPhase onSaleProcessPhase;
@Override
public Response execute(OnSaleNormalItemCmd cmd) {
OnSaleContext onSaleContext = init(cmd);
checkData(onSaleContext);
process(onSaleContext);
return Response.buildSuccess();
}
private OnSaleContext init(OnSaleNormalItemCmd cmd) {
return onSaleContextInitPhase.init(cmd);
}
private void checkData(OnSaleContext onSaleContext) {
onSaleDataCheckPhase.check(onSaleContext);
}
private void process(OnSaleContext onSaleContext) {
onSaleProcessPhase.process(onSaleContext);
}
}

每个 Phase 又可以拆解成多个步骤(Step),以 OnSaleProcessPhase 为例,它是由一系列 Step 组成的:

@Phase
public class OnSaleProcessPhase {
@Resource
private PublishOfferStep publishOfferStep;
@Resource
private BackOfferBindStep backOfferBindStep;
//省略其它step
public void process(OnSaleContext onSaleContext){
SupplierItem supplierItem = onSaleContext.getSupplierItem();
// 生成OfferGroupNo
generateOfferGroupNo(supplierItem);
// 发布商品
publishOffer(supplierItem);
// 前后端库存绑定 backoffer域
bindBackOfferStock(supplierItem);
// 同步库存路由 backoffer域
syncStockRoute(supplierItem);
// 设置虚拟商品拓展字段
setVirtualProductExtension(supplierItem);
// 发货保障打标 offer域
markSendProtection(supplierItem);
// 记录变更内容ChangeDetail
recordChangeDetail(supplierItem);
// 同步供货价到BackOffer
syncSupplyPriceToBackOffer(supplierItem);
// 如果是组合商品打标,写扩展信息
setCombineProductExtension(supplierItem);
// 去售罄标
removeSellOutTag(offerId);
// 发送领域事件
fireDomainEvent(supplierItem);
// 关闭关联的待办事项
closeIssues(supplierItem);
}
}

因此,在做过程分解的时候,我建议工程师不要把太多精力放在工具上,放在设计模式带来的灵活性上。而是应该多花时间在对问题分析,结构化分解,最后通过合理的抽象,形成合适的阶段(Phase)和步骤(Step)上。

对象模型

使用过程分解之后的代码,已经比以前的代码更清晰、更容易维护了。不过,还有两个问题值得我们去关注一下。首先是领域知识被割裂肢解:过程化拆解,导致没有一个聚合领域知识的地方。每个 Use Case 的代码只关心自己的处理流程,知识没有沉淀。相同的业务逻辑会在多个 Use Case 中被重复实现,导致代码重复度高,即使有复用,最多也就是抽取一个 util,代码对业务语义的表达能力很弱,从而影响代码的可读性和可理解性。

其次是代码的业务表达能力缺失,举个例子,在上架过程中,有一个校验是检查库存的,其中对于组合品(CombineBackOffer)其库存的处理会和普通品不一样。原来的代码是这么写的:

boolean isCombineProduct = supplierItem.getSign().isCombProductQuote();
// supplier.usc warehouse needn't check
if (WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())) {
// quote warehosue check
if (CollectionUtil.isEmpty(supplierItem.getWarehouseIdList()) && !isCombineProduct) {
throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");
}
// inventory amount check
Long sellableAmount = 0L;
if (!isCombineProduct) {
sellableAmount = normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(), supplierItem.getWarehouseIdList());
} else {
//组套商品
OfferModel backOffer = backOfferQueryService.getBackOffer(supplierItem.getBackOfferId());
if (backOffer != null) {
sellableAmount = backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale();
}
}
if (sellableAmount < 1) {
throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + supplierItem.getId() + "]");
}
}

然而,如果我们在系统中引入领域模型之后,其代码会简化为如下:

if(backOffer.isCloudWarehouse()){
return;
}
if (backOffer.isNonInWarehouse()){
throw new BizException("亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");
}
if (backOffer.getStockAmount() < 1){
throw new BizException("亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + backOffer.getSupplierItem().getCspuCode() + "]");
}

使用模型的表达要清晰易懂很多,而且也不需要做关于组合品的判断了,因为我们在系统中引入了更加贴近现实的对象模型(CombineBackOffer 继承 BackOffer),通过对象的多态可以消除我们代码中的大部分的 if-else。

通过上面的案例,我们可以看到有过程分解要好于没有分解,过程分解+对象模型要好于仅仅是过程分解。对于商品上架这个 case,如果采用过程分解+对象模型的方式,最终我们会得到一个如下的系统结构: