一文教会你如何写复杂业务代码

简介: 这两天在看零售通商品域的代码。面对零售通如此复杂的业务场景,如何在架构代码层面进行应对,是一个新课题。针对该命题,我进行了比较细致的思考和研究。结合实际$ C g R H 1 r的业% W t ( . O a f务场景,我沉淀了一套“如何写复杂业务代码”的方法论,在此分享给大家。

一文教会你如何写复杂业务代码

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

9 A ?两天在看零售通商品域的: ) E代码。面对零售U ) )通如此V V u G m : # #复杂的业务场景,如何在架构和代码层面进行应对,是一个新课题。针对该命题,我进行了比较细致的思考和研究。结合实际的业务场景,我沉淀了一套“如何写复杂业务代码”的方法论,在此分享给大家。

我相信,同样的方法论可以复制到大部分复杂业务场景。

一个复杂业务的处理过程

业务背景

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

一文教会你如何写复杂业务代码

商品力是零售通的核心所在,一个商品在零售通的生命周期如下图所示:

一文教会你如何写复杂业务代码

在上图中红U J q }框标识A V i p F u V ^的是一个运营操作的“上架”动作,这是非常关键的业务操作。上架之后,商品就能在零售通上面对小店进行销售了。因为上架操作非常关键,所以也是商品域中u 7 V { % 5 @最复杂的业务之一,涉及很多的数据校验和关联操作

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

一文教会你如何写复杂业务代码

过程分解

像这么复杂的业务,我想应该没有人会写在一G Y D E ? a t个 service 方法中吧。一个类解决不了,那就分治吧。

说实话[ ~ j k m D h ],能想到分而治之的工程师,已经做的不错了,至少比没有分治思维要好很多。我也见过复杂程度相当的业务,连分解都没有,就是一堆方法和类的堆砌。

不过,这里存在一个问题:即很多同学过度的依赖工具或} F & E是辅助手h p } r c 1 K 1段来实现分解。比如在我们c i s r 7 O W ` d的商品域中,类似的分解手段至少有 3 套以上,有自制的流程引擎,有依赖于数据库配置的流程处理:

一文教会你如何写复杂业务代码

本质上来讲,这些辅助手段做的都是一个 pipB o Reline 的处理流程,没有其它。因此,我建议此处最好t t - x D D 6保持 KISS(Keep It Simple a^ G I / ^nd Stupid),即最好是什j @ l么工具都不要用,次之是用一个极简的 Pipeline 模式,最差是使用像流程引擎这样B ? a的重方法

除非你的应用有极r : Y , ( V强的流程可视x b u P 3 S p化和编排的诉求,否则我非常不推荐使用流程引擎等工具。第一,它= U ; M会引入额外的复杂度,特别是那些需要持久化状态的流程引擎;第二,它会割裂代码,导致阅读代码的不顺畅。大胆断言一下,全天下估计 80% 对流程引擎的使用都是j + s j M V F @得不偿失的

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

一文教会你如何写复杂业务代码

按照这种分解写的代码,就像一本书,目录和内容p | V y c清晰明了。

以商品上架为例,程序的入口是一个上架命令(OnSaleComy 6 ; 3 I Y 4 Wm- e V y . & U = (and), 它由三个阶段(Phac s , ) .se)组成。

@Command

public class OnSaleNormalItemCmdExe {

@ResourceQ x H ; `

pro M P ] ivate OnSaleContextInitPhase onSaleContextInitPhase;

@ResouR i 7 Y }rce

private OnSalj O @ 9 d # 2 ^eDataCheckPhase onSaleDataCheckPhase;

@Resource

private OnSaleProcessPhase onSaleProcessPhase;

@Override

public Response execute(OnSaleNormalItemCmdS P N H I + l S cmd) {

OnSaleContext onSaleContext = init(cmd);

checkData(onSaleContext);

process(onSaleContext);

return Response.buildSuccess();

}

priva( ^ _te OnSaleContext init(OnSaleNo } V B c a ,rmalItemCmd cmd) {) ` ] h l { Z j

return onSaleContextInitPhase.init(cmd);

}

private void checkData(OnSaleContext onSaleContext) {

onSaleDataCheckPhase.check(onSaleContext);] ( K k

}

private void proS q L D =cess(OnSaleContext onSaleContext) {

ond x ! h U R ZSalePr? 2 tocessPhase.process(onSaleContext);

}

}

每个 Phase 又可以拆解E Q 6 - u H | a成多个步骤(Step),以 OnSaleProcessPhase 为例,它是由一系列 Sten ~ 3 | - Kp 组成的:J * F C

@Phase

public class OnSaleProceq T ( a 0 } ] T lssPhase {

@Resource

private PublishOfferStep publishOfferStep;

@Resource

private BackOfferBindStep backOfferBindStep;

//省略其它step

public void process(OnSaleContext onSaleContext){

SupplierItem supplierItem = onSaleContext.getSupplierItem();

// 生成OfferGroupNo

generateOfferGroupNo(supplierItem);

// 发布商品

publishOffer(supplierh y P # Z E A R zItem);

// 前后端库存c Z 1绑定 backoffer域

bindBacu = 3 t MkOfferStock(suppli7 _ P R z D JerItem);

// 同步库存路由 backoffer域

syncStockRoute(supplierItem);

// 设F - F $ 3 v置虚拟商品拓展字段

setVirtualProductExtension(supplierItem);

// 发货保障打标 offer域

markSI s ^ 4 w ] V R 6endProte 1 1 k S 1 S e ction(supplierItem);

// 记录变更内容ChangeDetail

recordChangeDetail(supplierT y 3 Y g G 1 NItem);

// 同步供货价到BackOffer

sync* k a ? BSupplyPriceToBackOffer(supplierItem);

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

setCombineProductExtensl I x Y I O x oion(supplierItem);

// 去售罄标

removeSellOutTag(offerId);

// 发送领域事件

fireDomainEveW P G 9 R ~nt(supplierItem);

// 关闭关联的待办事项

closeIssues(supplierItem);

}

}

看到了吗,这就是商品上架这个复杂业务的业务流程。需要流程引擎吗?不需要;需要设计模式支撑吗?也不需要。对g Q 2 G于这种业e o 3 z H务流程的表达,简单朴素的组合方法模式q M l d G(Composed Method)是再合适不过的了。

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

一文教会你如何写复杂业务代码

过程分解后的两个问题

的确,使用过程分解之后的代码,已经比以前的代码更清晰、更容易维护了。不过,还有两个问题值得我们去关注一下:

1、领域知识被割裂肢解

什么叫被肢解?因为我们到目前为止做的都是过程化拆解,导致没有一个聚合领域知识的地方。每个 Use Case 的代码只关心自己的处理流程,知识/ s z ~没有沉淀。

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

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

试想下,在过程式的代码中,所做的事情无外乎就是取数据 -- 做计算 -- 存数据,在这种情况下,要如何通过代码显性化的表达我们的业务呢? 说实话,很难做到,因为我们缺失了模型,以及模型之间的关系。脱离模型的业务表达,是缺少韵律和灵魂的。 举个例子,在上架过程中,有一个校验是检查库存的,其中对于组合品(CombineBackOffer)其库存的处理会和普通品不一样。原来的代码是这L 3 Y a 3 T y +么写的:

boolean isCombineQ W TProdY 2 { p k : B 6uct = supplierItem.getSign().isCombPrO + d I 7 eoductQuote();

// supplier.usc war^ 1 5 p X fehouse needn't check

if (WarehouseTypeEnum.isAliWarehouse(s0 k e ^upplierItem.getWarehouseType())) {

// q{ S Iuote warehosue check

if (CollectionUtil.isEmpty(supplierItem.geg . v @ d z ^tWare( 5 c FhouseIdList()) &aR # L U } !mp;& !isCombineProduct) {

throw ExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR, "亲,不能发布Offer,请联系仓配运营人员,建立品仓关系!")? C B | b V : Q;

}

// inventory amount check

Long sellableAmount = 0L;

if (!isCom( b E kbineProduct) {

sellableAmount = normalBiz.acquireSellabl! b } @ ;eAmount(supplierItem.getBackOfferIH 1 - 6 5 0d(), suppliD v U & X 8erItem.gu . cetWarehouseIdList());

} else {

//组套商品

OfferModel bG S = a : 2ackOffer = bq + )ackOfferQueryService.getBackOffer(supplierItem.getBackOfferId());

if (backOffer !K & ~= null) {

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

}

}

if (sellablf % s [eAmount &w a ylt; 1) {

throw ExcQ 4 n ! v d V u 6eptionFactory.makeFaD E n -ult(ServiceExceptionCode.SYSTEM_ERRO, 5 6 R g 2 y qR, "亲,实仓库存必须大h 1 e i e m h U于0才能发布,请确认, b d | , L 6 X F已补货.\r[id:" + supplierIB ^ o Ltem.getId() + "]");

}

}

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

if(ba 6 n 2 E B z ? WackOffer.isCloudWarehouse()){

return;

}

if (backOffer.isNonInWarehouse()){

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

}

if (backOffeu 6 $ $ y L 3 qr.getStox S w ; 3 7 (ckAmx 7 k ,ount() < 1){

throw new BizException("亲,实仓库存必须大于0才能发布,请确认已补货.\r[id:" + bP 0 , j m Q * T )ackOffer.getSupplierItem().getCspuCode() + ~ . V 1 L O"]");

}

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

一文教会你如何写复杂业务代码

过程分解+对象模型

通过上面的案L i . d E 7 i例,我们可以看到有过程分解要好于没有分解过程分解+对象模型要好于仅仅是过程分解。对于商品上架这个 case,如果采用过程@ v y 2 J分解+对象模型的方式,最终我们会得到一a 1 o { g个如下的系统结构:

一文教会你如何写复杂业务代码

写复杂业务的方法论

通过上面案例的讲解,我想说,我已经{ V ; H ] [ i [ !交代了复杂业务代码要怎么写:即自上而下的结构化分解+自下而上的面向对象分析

接下来,让我们把上面的案例进行进一步的提炼,形成一个可落地的方法论,从而可以泛化到更多的复杂业务场景。- { 2 % ^ A c M W

上下结合

所谓上下g - u X T 1结合,是指我们要结合自上而下的过程分解和自下而上的对象建模,螺旋式的构建我们的应用系统。这是一个动态的过程,两个步骤可以交替进行、也可以同时进行。

这两个步骤是相辅相成的,上面的分析可T / J以帮助我们更好的理清模型之间的关系,而A J +下面的模型表达可以提升我们代码的复用度和业务语义表达能力

其过程如下图所示:

一文教会你如何写复杂业务代码

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

能力下沉s ` ] z G u

一般C f G x来说实践v M 0 DDD0 C % [ / ^ 有两个过程3 o ! P p + b 2

1. 套概念阶段

了解了一些 DDD 的概念,然后在 A . T W代码中“使用”Aggregation Root,Bounded Context,Reposi^ i . )tory 等等这些概念。更进一步,j $ u = m I也会使用j T = ] ) i g一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。

2.g ? ~ ) e T S | 融会贯通阶段

t g i w D语已经不再重要,理解 DDD 的本质是统一语言、边界划分和H ] $ & , i面向对象分析的方法。

大体上而言,我大概是在 1.7 的阶段,因为有一个问题一直在困扰我,就是哪些能7 2 Z f $ m 3 a s力应该放在 Domain 层,是不是按照传统的做8 B 8 a b J o法,将所有的业务都收拢到 Domain 上,这样做合理吗?说实话,这个问题我I @ f Z - G一直没有想清楚。

因为在现实业务中,很多的功能都是用例特有的(Use case speciT ( = ^fic)的,如果“盲目”的使用 Domain 收拢业务并不见得能带来多大的益处。相反,这种收拢会导致 Domain 层的膨胀过厚,不够纯粹,反而会影响复用性和表达能力。1 j h 0 T 8 U d V

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

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

注:Use Case 是《架构整洁之道》里面的术语,简单理解就是响应一个 Request 的处理过程& O K X

通过实践,我发现这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。 *B 1 n 4* 下沉的过程如下t % ) t ` j图所示,假设两个 use case 中,我们发现 uc1 的 step3 和 uc2- , c ^ Y b 的 step1 有类似的功能,我们就可以考虑让其下沉到 Domain 层,从而增加代码的复用性。

一文教会你如何写复杂业务代码

指导下沉有两个关键H h C P X指标:代码的复用性和内聚性。

复用性是告诉我们 When(什么时候该: t x F W下沉了),即有重复代码的时候。内W _ [ W } { G聚性是告诉我们 How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上(因为 Domain 层i P ~ k x V 的能力也是有两个层次的,. a 1一个是 Domain Service 这是相对比+ H ^ & S V较粗的粒度,另一个是 Domain 的 Model 这个是最细粒度的复用)。

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

pug T Yblic class CSPU {

private String code;

private String baseCode;

//省略其它属性

/**

* 单品是否为最小单位。

*/

public boolean isMinimumUnit(){

return String+ T , =Utils.equals(code, baseCode);

}

/**

* 针对中包的特殊处理

*/

public boolean isMidPackage(){

return StringUtils.equals(coC k - 5 M k + sde, midPackageCode);

}

}

之前,因为老系统中没有领域模型,没有 CSPU 这个实体。你会发现像判断单品是否为最小单位的逻辑是以 St2 L uringUtils.equals(code, ba| g ! J v Y l seCS ; x S ;ode) 的形式散落在代码的各个角落。这种代码的可理解性是a 2 _ & s d m 0可想而知的,至少我在第一眼看到这个代码的时候,是完全不知道什么意思。

业务技术要怎么做

写到这里,我想顺便回答一下很多业务技术同学的困惑,也是我之前的困惑:即业务技术到底是在做业务,还是做技术s Y f?业务技8 u } j 6 0 ^ L ]术的技术性体现在; ) $ . B F . ) Q哪里?

通过上面的案例,我们可以看到业务所面临的复杂性并不亚于底层技术,要想写好业务代码也不是一件容易的J H @ v + . (事情。业务技术和底层技术人员唯一的区X m / k别是他们所面临的问题域不一样。

业务技术面对的问题域变化更多、面对的人更加庞杂。而底层技术面对的问f ) w题域更加稳定、但对技术的要求更加深。比如,如果你需要去开发 Pandp L h s M - Yora,你就要对 Classloader 有更加W y ! Y深入的了解才行。

但是I b 4 E V d k U,不管是业务技术还是底层技术人员,有一些思维和能力都是共通的。比如,分解问题的能力,抽象思维,结构化思维等等。

一文教会你如何写复杂业务代码

用我的话说就是:“做不好业务开发的,也做不好技术底层开发,反m * q l F P y之亦然。业务$ g a r | , _ 1开发一点都不简单,只是我们很多人+ h Q 6把它做Q h H A : *“简单”了

因此,如果从变化的角度来看,业务技术的难度一点不逊色于底层技术,其面临的挑战甚至更大。y , m 2因此,我想对广大的从事业务技术开发的同学说:沉下心来,夯实自己J n ]的基础技术能* A U l W %力、OO 能力、建模能力... 不断提升抽象思维、结构化思维、思辨思维... 持续学习精进,写好代码。我们可以在业务技术岗做的很”技术“!

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

本文为阿里云原创内容,未经允许不得转载