关于程序员:一文教会你如何写复杂业务代码

41次阅读

共计 7003 个字符,预计需要花费 18 分钟才能阅读完成。

简介: 这两天在看批发通商品域的代码。面对批发通如此简单的业务场景,如何在架构和代码层面进行应答,是一个新课题。针对该命题,我进行了比拟粗疏的思考和钻研。结合实际的业务场景,我积淀了一套“如何写简单业务代码”的方法论,在此分享给大家。

作者 | 张建飞  阿里巴巴高级技术专家

理解我的人都晓得,我始终在致力于利用架构和代码复杂度的治理。

这两天在看批发通商品域的代码。面对批发通如此简单的业务场景,如何在架构和代码层面进行应答,是一个新课题。针对该命题,我进行了比拟粗疏的思考和钻研。结合实际的业务场景,我积淀了一套“如何写简单业务代码”的方法论,在此分享给大家。

我置信,同样的方法论能够复制到大部分简单业务场景。

一个简单业务的处理过程

业务背景

简略的介绍下业务背景,批发通是给线下小店供货的 B2B 模式,咱们心愿通过数字化重构传统供应链渠道,晋升供应链效率,为新批发助力。阿里在两头是一个平台角色,提供的是 Bsbc 中的 service 的性能。

商品力是批发通的外围所在,一个商品在批发通的生命周期如下图所示:

在上图中红框标识的是一个经营操作的“上架”动作,这是十分要害的业务操作。上架之后,商品就能在批发通上面对小店进行销售了。因为上架操作十分要害,所以也是商品域中最简单的业务之一,波及很多的数据校验和关联操作

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

过程合成

像这么简单的业务,我想应该没有人会写在一个 service 办法中吧。一个类解决不了,那就分治吧。

说实话,能想到分而治之的工程师,曾经做的不错了,至多比没有分治思维要好很多。我也见过复杂程度相当的业务,连合成都没有,就是一堆办法和类的堆砌。

不过,这里存在一个问题:即很多同学适度的依赖工具或是辅助伎俩来实现合成。比方在咱们的商品域中,相似的合成伎俩至多有 3 套以上,有自制的流程引擎,有依赖于数据库配置的流程解决:

实质上来讲,这些辅助伎俩做的都是一个 pipeline 的解决流程,没有其它。因而,我倡议此处最好放弃 KISS(Keep It Simple and Stupid),即 最好是什么工具都不要用,次之是用一个极简的 Pipeline 模式,最差是应用像流程引擎这样的重办法

除非你的利用有极强的流程可视化和编排的诉求,否则我十分不举荐应用流程引擎等工具。第一,它会引入额定的复杂度,特地是那些须要长久化状态的流程引擎;第二,它会割裂代码,导致浏览代码的不顺畅。大胆断言一下,全天下预计 80% 对流程引擎的应用都是得失相当的

回到商品上架的问题,这里问题外围是工具吗?是设计模式带来的代码灵活性吗?显然不是,问题的外围应该是如何合成问题和形象问题,晓得金字塔原理的应该晓得,此处,咱们能够应用结构化合成将问题解形成一个有层级的金字塔构造:

依照这种合成写的代码,就像一本书,目录和内容清晰明了。

以商品上架为例,程序的入口是一个上架命令(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);
    }
}

看到了吗,这就是商品上架这个简单业务的业务流程。须要流程引擎吗?不须要;须要设计模式撑持吗?也不须要。对于这种业务流程的表白,简略奢侈的组合办法模式(Composed Method)是再适合不过的了。

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

过程合成后的两个问题

确实,应用过程合成之后的代码,曾经比以前的代码更清晰、更容易保护了。不过,还有两个问题值得咱们去关注一下:

1、畛域常识被割裂肢解

什么叫被肢解?因为咱们到目前为止做的都是过程化拆解,导致没有一个聚合畛域常识的中央。每个 Use Case 的代码只关怀本人的解决流程,常识没有积淀。

雷同的业务逻辑会在多个 Use Case 中被反复实现,导致代码反复度高,即便有复用,最多也就是抽取一个 util,代码对业务语义的表达能力很弱,从而影响代码的可读性和可了解性。

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

试想下,在过程式的代码中,所做的事件无外乎就是取数据 — 做计算 — 存数据,在这种状况下,要如何通过代码显性化的表白咱们的业务呢?说实话,很难做到,因为咱们缺失了模型,以及模型之间的关系。脱离模型的业务表白,是短少韵律和灵魂的。
举个例子,在上架过程中,有一个校验是查看库存的,其中对于组合品(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,如果采纳过程合成 + 对象模型的形式,最终咱们会失去一个如下的系统结构:

写简单业务的方法论

通过下面案例的解说,我想说,我曾经交代了简单业务代码要怎么写:即自上而下的结构化合成 + 自下而上的面向对象分析

接下来,让咱们把下面的案例进行进一步的提炼,造成一个可落地的方法论,从而能够泛化到更多的简单业务场景。

上下结合

所谓上下结合,是指咱们要 联合自上而下的过程合成和自下而上的对象建模,螺旋式的构建咱们的利用零碎。这是一个动静的过程,两个步骤能够交替进行、也能够同时进行。

这两个步骤是相辅相成的,下面的剖析能够帮忙咱们更好的理清模型之间的关系,而上面的模型表白能够晋升咱们代码的复用度和业务语义表达能力

其过程如下图所示:

应用这种上下结合的形式,咱们就有可能在面对任何简单的业务场景,都能写出洁净整洁、易保护的代码。

能力下沉

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

1. 套概念阶段

理解了一些 DDD 的概念,而后在代码中“应用”Aggregation Root,Bounded Context,Repository 等等这些概念。更进一步,也会应用肯定的分层策略。然而这种做法个别对复杂度的治理并没有多大作用。

2. 死记硬背阶段

术语曾经不再重要,了解 DDD 的实质是对立语言、边界划分和面向对象分析的办法。

大体上而言,我大略是在 1.7 的阶段,因为有一个问题始终在困扰我,就是哪些能力应该放在 Domain 层,是不是依照传统的做法,将所有的业务都收拢到 Domain 上,这样做正当吗?说实话,这个问题我始终没有想分明。

因为在事实业务中,很多的性能都是用例特有的(Use case specific)的,如果“自觉”的应用 Domain 收拢业务并不见得能带来多大的好处。相同,这种收拢会导致 Domain 层的收缩过厚,不够纯正,反而会影响复用性和表达能力。

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

所谓的能力下沉,是指咱们不强求一次就能设计出 Domain 的能力,也不须要强制要求把所有的业务性能都放到 Domain 层,而是采纳实用主义的态度,即只对那些须要在多个场景中须要被复用的能力进行形象下沉,而不须要复用的,就临时放在 App 层的 Use Case 里就好了。

注:Use Case 是《架构整洁之道》外面的术语,简略了解就是响应一个 Request 的处理过程。

通过实际,我发现这种循序渐进的能力下沉策略,应该是一种更符合实际、更麻利的办法。因为咱们抵赖模型不是一次性设计进去的,而是迭代演变进去的。
**
下沉的过程如下图所示,假如两个 use case 中,咱们发现 uc1 的 step3 和 uc2 的 step1 有相似的性能,咱们就能够思考让其下沉到 Domain 层,从而减少代码的复用性。

领导下沉有两个要害指标:代码的复用性和内聚性。

复用性是通知咱们 When(什么时候该下沉了),即有反复代码的时候。内聚性是通知咱们 How(要下沉到哪里),性能有没有内聚到失当的实体上,有没有放到适合的档次上(因为 Domain 层的能力也是有两个档次的,一个是 Domain Service 这是绝对比拟粗的粒度,另一个是 Domain 的 Model 这个是最细粒度的复用)。

比方,在咱们的商品域,常常须要判断一个商品是不是最小单位,是不是中包商品。像这种能力就十分有必要间接挂载在 Model 上。

public class CSPU {
    private String code;
    private String baseCode;
    // 省略其它属性

    /**
     * 单品是否为最小单位。*
     */
    public boolean isMinimumUnit(){return StringUtils.equals(code, baseCode);
    }

    /**
     * 针对中包的非凡解决
     *
     */
    public boolean isMidPackage(){return StringUtils.equals(code, midPackageCode);
    }
}

之前,因为老零碎中没有畛域模型,没有 CSPU 这个实体。你会发现像判断单品是否为最小单位的逻辑是以 StringUtils.equals(code, baseCode) 的模式散落在代码的各个角落。这种代码的可了解性是可想而知的,至多我在第一眼看到这个代码的时候,是齐全不晓得什么意思。

业务技术要怎么做

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

通过下面的案例,咱们能够看到业务所面临的复杂性并不亚于底层技术,要想写好业务代码也不是一件容易的事件。业务技术和底层技术人员惟一的区别是他们所面临的问题域不一样。

业务技术面对的问题域变动更多、面对的人更加庞杂。而底层技术面对的问题域更加稳固、但对技术的要求更加深。比方,如果你须要去开发 Pandora,你就要对 Classloader 有更加深刻的理解才行。

然而,不论是业务技术还是底层技术人员,有一些思维和能力都是共通的。比方,合成问题的能力,抽象思维,结构化思维 等等。

用我的话说就是:“做不好业务开发的,也做不好技术底层开发,反之亦然。业务开发一点都不简略,只是咱们很多人把它做“简略”了

因而,如果从变动的角度来看,业务技术的难度一点不逊色于底层技术,其面临的挑战甚至更大。因而,我想对宽广的从事业务技术开发的同学说:沉下心来,夯实本人的根底技术能力、OO 能力、建模能力 … 一直晋升抽象思维、结构化思维、思辨思维 … 继续学习精进,写好代码。咱们能够在业务技术岗做的很”技术“!

正文完
 0