了解我的人都知道,我一直在致力于应用架构和代码复杂度的治理。

这两天在看零售通商品域的代码。面对零售通如此复杂的业务场景,如何在架构和代码层面进行应对,我有了一些新的思考,在此分享给大家。我相信,同样的方法论可以复制到大部分复杂业务场景。

一个复杂业务的处理过程

业务背景

简单的介绍下业务背景,零售通是给线下小店供货的B2B模式,我们希望通过数字化重构传统供应链渠道,提升供应链效率,为新零售助力。阿里在中间是一个平台角色,提供的Bsbc中的service的功能。

复杂业务代码要怎么写_java

在商品域,运营会操作一个“上架”动作,上架之后,商品就能在零售通上面对小店进行销售了。是零售通业务非常关键的业务操作之一,因此涉及很多的数据校验和关联操作。

针对上架,一个简化的业务流程如下所示:

复杂业务代码要怎么写_java_02

过程分解

像这么复杂的业务,我想应该没有人会写在一个service方法中吧。

一个方法解决不了,那就分治吧。说实话,能想到分而治之的工程师,已经做的不错了,至少比哪些没有分治思维的工程师好。我也见过复杂程度相当的业务,连分解都没有,就是一堆方法和类的堆砌。

然而,这里也有一个不好的现象,就是很多同学过度的依赖工具来实现这样的分解。比如在我们的商品域中,类似的分解工具就有3套,有自制的流程引擎,有依赖于数据库做配置的。  

本质上来讲,他们做的都是一个pipeline的处理流程,这个地方最好尽量保持KISS(Keep It Simple and Stupid),即最好是什么工具都不要用,次之是用一个极简的Pipeline模式,最差是使用像流程引擎这样的重方法

除非你的应用有极强的流程可视化和编排的诉求,否则我非常不推荐使用流程引擎等工具。第一,它会引入额外的复杂度,特别是那些需要持久化状态的流程引擎。第二,它会割裂代码,导致阅读代码的不顺畅。大胆断言一下,全天下估计80%对流程引擎的使用都是得不偿失的。

回到“上架”的问题,这里的问题核心是对其业务过程进行如下的结构化分解,形成一个金字塔结构:

然后使用朴素的组合方法模式(Composed Method),将代码写成如下的式样就OK了。

  1. @Phase

  2. public class OnSaleProcessPhase {


  3.    @Resource

  4.    private PublishOfferStep publishOfferStep;

  5.    @Resource

  6.    private BackOfferBindStep backOfferBindStep;

  7.    //省略其它step


  8.    public void process(OnSaleContext onSaleContext){

  9.        SupplierItem supplierItem = onSaleContext.getSupplierItem();


  10.        // 生成OfferGroupNo

  11.        generateOfferGroupNo(supplierItem);


  12.       // 发布商品

  13.        publishOffer(supplierItem);


  14.        // 前后端库存绑定 backoffer域

  15.        bindBackOfferStock(supplierItem);


  16.        // 同步库存路由 backoffer域

  17.        syncStockRoute(supplierItem);


  18.        // 设置虚拟商品拓展字段

  19.        setVirtualProductExtension(supplierItem);


  20.        // 发货保障打标 offer域

  21.        markSendProtection(supplierItem);


  22.        // 记录变更内容ChangeDetail

  23.        recordChangeDetail(supplierItem);


  24.        // 同步供货价到BackOffer

  25.        syncSupplyPriceToBackOffer(supplierItem);


  26.        // 如果是组合商品打标,写扩展信息

  27.        setCombineProductExtension(supplierItem);


  28.        // 去售罄标

  29.        removeSellOutTag(offerId);


  30.        // 发送领域事件

  31.        fireDomainEvent(supplierItem);


  32.        // 关闭关联的待办事项

  33.        closeIssues(supplierItem);

  34.    }

  35. }

因此,在做过程分解的时候,我建议工程师不要把精力放在工具上,而是多花时间放在对问题的分析上,然后通过合理的抽象,形成合适的阶段(Phase)和步骤(Step)即可。复杂业务代码要怎么写_java_03

过程分解后的两个问题

使用过程分解之后的代码,肯定会比以前的代码更有设计感,更容易维护。不过,有两个问题需要我们去关注。

1、领域知识被割裂肢解

什么叫被割裂?因为我们到目前为止做的都是过程化拆解,导致没有一个聚合领域知识的地方。每个Use Case的代码只关心自己的处理流程,知识没有沉淀,代码重复度高。同样的业务逻辑会在多个Use Case中被重复实现。

2、代码的业务表达能力缺失

试想下,在过程式的代码中,所做的事情无外乎就是取数据--做计算--存数据,在这种情况下,要如何通过代码显性化的表达我们的业务呢?说实话,很难做到,因为我们缺失了模型,已经模型之间的关系。

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

  1. boolean isCombineProduct = supplierItem.getSign().isCombProductQuote();


  2. // supplier.usc warehouse needn't check

  3. if (WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())) {

  4. // quote warehosue check

  5. if (CollectionUtil.isEmpty(supplierItem.getWarehouseIdList()) && !isCombineProduct) {

  6.    throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");

  7. }

  8. // inventory amount check

  9. Long sellableAmount = 0L;

  10. if (!isCombineProduct) {

  11.    sellableAmount = normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(), supplierItem.getWarehouseIdList());

  12. } else {

  13.    //组套商品

  14.    OfferModel backOffer = backOfferQueryService.getBackOffer(supplierItem.getBackOfferId());

  15.    if (backOffer != null) {

  16.        sellableAmount = backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale();

  17.    }

  18. }

  19. if (sellableAmount < 1) {

  20.    throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + supplierItem.getId() + "]");

  21. }

  22. }

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

  1. if(backOffer.isCloudWarehouse()){

  2.    return;

  3. }


  4. if (backOffer.isNonInWarehouse()){

  5.    throw new BizException("亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!");

  6. }


  7. if (backOffer.getStockAmount() < 1){

  8.    throw new BizException("亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + backOffer.getSupplierItem().getCspuCode() + "]");

  9. }  

有没有发现,使用模型的表达要清晰易懂很多,而且关于组合品的判断也没有了,因为我们引入了如下的对象模型,而对象的多态可以消除我们代码中的很多if-else。 

过程分解+对象模型

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

写复杂业务的方法论

通过上面案例的讲解,我想说,我已经交代了复杂业务代码要怎么写:即自上而下的结构化分解+自下而上的面向对象分析。接下来,让我们把上面的案例进行进一步的提炼,形成一个可记忆、可落地的方法论,从而可以泛化到更多的复杂业务场景。

上下结合

所谓上下结合,是指我们要结合自上而下的过程分解和自下而上的对象建模。这两个步骤可以交替进行、也可以同时进行。这两个步骤是相辅相成的,上面的分析可以帮助我们更好的理清模型之间的关系,而下面的模型表达可以提升我们代码的复用度和业务语义表达能力。

其过程如下图所示:复杂业务代码要怎么写_java_04

使用这种上下结合的方式,我们就有可能在面对任何复杂的业务场景,都能写出干净整洁、易维护的代码。

能力下沉

一般来说实践DDD有两个过程:

1.套概念阶段:了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bonded Context,Repository等等这些概念。跟进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。

2.融会贯通阶段:术语已经不再重要,理解DDD的本质就是面向对象分析问题的方法论。

大体上而言,我大概是在1.7的阶段,因为有一个问题还在一直困扰我,就是哪些能力应该放在Domain层,我始终没有思考清楚。按照传统的理解,所有的业务逻辑都应该收拢在Domain层。这句话太笼统,而且也不一定正确。

真实场景是很多功能都是用例特有的(Use case specific)的,而这种“盲目”的使用Domain收拢业务并没有带来多大的益处。相反,这种收拢会导致Domain层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。

鉴于此,我最近的思考是我们应该采用**能力下沉”的策略。

所谓的能力下沉,是指我们不要强求一次就能设计出Domain的能力,而是通过不断的迭代,逐渐让需要被复用的能力下沉到Domain层,是一个循序渐进的过程。如下图所示:复杂业务代码要怎么写_java_05

从图中我们可以看到,在两个use case中,我们发现uc1的step3和uc2的step1有类似的功能,我们就可以考虑让其下沉到Domain层,从而增加代码的复用性。

Domain层的能力也有两个层次,一个是Domain Service这是相对比较粗的粒度,另一个是Domain的Model这个是最细粒度的复用。

比如,在我们的商品域,经常需要判断一个商品是不是最小单位,是不是中包商品。像这种能力就非常有必要直接挂载在Model上。

  1. public class CSPU {

  2.    private String code;

  3.    private String baseCode;

  4.    //省略其它属性


  5.    /**

  6.     * 单品是否为最小单位。

  7.     *

  8.     */

  9.    public boolean isMinimumUnit(){

  10.        return StringUtils.equals(code, baseCode);

  11.    }


  12.    /**

  13.     * 针对中包的特殊处理

  14.     *

  15.     */

  16.    public boolean isMidPackage(){

  17.        return StringUtils.equals(code, midPackageCode);

  18.    }

  19. }

因为,当系统中没有领域模型的概念,没有CSPU的时候。你会发现像判断单品是否为最小单位的逻辑是以 StringUtils.equals(code,baseCode)的形式散落在代码的各个角度。这种代码的可读性和可维护性可想而知,反正第一眼看到的时候,是完全不知其意。

业务技术的方向

写到这里,我想顺便回答一下很多业务技术同学的困惑,也是我之前的困惑:即业务技术到底是在做业务,还是做技术?业务技术的技术性体现在哪里?

通过上面的案例,我们可以看到业务所面临的复杂性并不亚于底层技术,要想写好业务代码也不是一件容易的事情。

业务技术和底层技术人员唯一的区别是他们面对的问题域不一样。

业务技术面对的问题域变化更多、面对的人更加庞杂。底层技术面对的问题域更加稳定、但对技术的要求更加深。比如,如果你需要去开发Pandora,你就要对Classloader了解的更加深入才行。都不是随随便便就能做好的。

但是,不管是业务技术还是底层技术人员,有一些思维和能力都是共通的。比如,分解问题的能力,抽象思维,结构化思维等等。复杂业务代码要怎么写_java_06

用我的话说就是:“做不好业务开发的,也做不好技术底层开发,反之亦然。工程师最大的美德是写好代码。”