当前位置: 首页 > 科技观察

阿里资深技术专家方法论:复杂的业务代码怎么写?

时间:2023-03-21 12:41:22 科技观察

张建飞,阿里巴巴高级技术专家,一直致力于应用架构和代码复杂度的治理。最近在看零售商品领域的代码。面对RetailLink如此复杂的业务场景,如何在架构和代码层面进行处理,是一个新的课题。Frank结合实际业务场景,总结出一套“如何编写复杂业务代码”的方法论,分享给大家。我相信可以在大多数复杂的业务场景中复制相同的方法。复杂的业务流程业务背景简单介绍一下业务背景。RetailExpress是一种为线下商店供货的B2B模式。我们希望对传统供应链渠道进行数字化重构,提升供应链效率,助力新零售。阿里是中间的平台角色,提供Bsbc中的服务功能。商品力是RetailLink的核心。RetailLink中一个产品的生命周期如下图所示:上图中,红框表示一个operation操作的“上架”动作,这是一个非常关键的业务操作。产品上架后,可以在零售通上销售给小店。因为listing操作非常关键,也是商品领域最复杂的业务之一,涉及到大量的数据校验和关联操作。对于listing,一个简化的业务流程如下:流程分解这么复杂的业务,我想没有人会写在一个service的方法里。如果一个类解决不了,那就分而治之。说实话,能想到分而治之的工程师已经做得很好了,至少比没有分而治之的思想要好很多。我也见过相当复杂的业务,连分解都没有,就是一堆方法和类。但是,这里存在一个问题:很多同学过于依赖工具或辅助手段来实现分解。比如我们的商品领域,至少有三套类似的分解方式,包括自制的流程引擎,依赖数据库配置的流程处理:这些辅助手段,本质上都是针对一个流水线处理流程。没有其他的。因此,我建议最好保持KISS(KeepItSimpleandStupid),即最好不要使用任何工具,其次是使用极简的Pipeline模式,最差的是使用重度方法如流程引擎。除非你的应用对流程可视化和编排有很强的需求,否则我不推荐使用流程引擎等工具。首先,它会引入额外的复杂性,特别是对于那些需要持久状态的流程引擎;其次,它会把代码分割开来,导致代码阅读不流畅。大胆断言,估计全球80%的流程引擎使用得不偿失。回到产品上架的问题,这里问题的核心是工具吗?是设计模式带来的代码灵活性吗?显然不是,问题的核心应该是如何分解问题,抽象问题。了解金字塔原理的应该都知道。在这里,我们可以使用结构化分解,将问题分解成一个层级金字塔结构:按照这种分解写出的代码就像一本书,内容清晰,内涵丰富。以上架商品为例,程序的入口是一条上架命令(OnSaleCommand),由三个阶段(Phase)组成。@CommandpublicclassOnSaleNormalItemCmdExe{@ResourceprivateOnSaleContextInitPhaseonSaleContextInitPhase;@ResourceprivateOnSaleDataCheckPhaseonSaleDataCheckPhase;@ResourceprivateOnSaleProcessPhaseonSaleProcessPhase;@OverridepublicResponseexecute(OnSaleNormalItemCmdcmd){OnSaleContextonSaleContext=init(cmd);checkData(onSaleContext);process(onSaleContext);returnResponse.buildSuccess();}privateOnSaleContextinit(OnSaleNormalItemCmdcmd){returnonSaleContextInitPhase.init(cmd);}privatevoidcheckData(OnSaleContextonSaleContext){onSaleDataCheckPhase.check(onSaleContext);}privatevoidprocess(OnSaleContextonSaleContext){onSaleProcessPhase.process(onSaleContext);}}每个Phase可以拆解成多个步骤(Step),以OnSaleProcessPhase为例,它由一系列Step组成:@PhasepublicclassOnSaleProcessPhase{@ResourceprivatePublishOfferSteppublishOfferStep;@ResourceprivateBackOfferBindStepbackOfferBindStep;//省略其他步骤publicvoidprocess(OnSaleContextonSaleContext){SupplierItemsupplierItem=onSaleContext.getSupplierItem();//生成OfferGroupNogenerateOfferGroupNo(supplierItem);//发布产品publishOffer(supplierItem);//绑定backoffer域bindBackOfferStock(supplierItem);//同步库存路由backoffer域syncStockRoute(supplierItem);//设置虚拟产品扩展字段setVirtualProductExtension(supplierItem);//保质保标报价字段markSendProtection(supplierItem);//记录变更内容ChangeDetailrecordChangeDetail(supplierItem);//同步货源价格到BackOfferssyncSupplyPriceToBackOffer(supplierItem);//如果是组合商品标示,写扩展informationsetCombineProductExtension(supplierItem);//去掉售罄标签removeSellOutTag(offerId);//发送领域事件fireDomainEvent(supplierItem);//关闭关联的待办事项closeIssues(supplierItem);}}看看是不是这个业务流程产品上市的复杂业务?它需要流程引擎吗?不是,需要设计模式支持吗?不需要。对于这种业务流程的表达,简洁明了的组合方法模式(ComposedMethod)堪称完美。所以,在做流程分解的时候,我建议工程师不要过分关注工具,而要关注设计模式带来的灵活性。相反,应该把更多的时间花在问题分析、结构分解上,最后通过合理的抽象,形成合适的阶段(Phase)和步骤(Step)。流程分解后的两个问题流程分解后的代码确实比之前的代码更清晰,更容易维护。但是,还有两个问题值得我们关注:领域知识碎片化、肢解化。什么是肢解?因为我们目前所做的都是程序拆解,导致没有地方聚合领域知识。每个UseCase的代码只关心自己的处理流程,知识不沉淀。相同的业务逻辑会在多个UseCases中重复实现,导致代码重复度很高。即使有复用,也最多抽取一个util。代码表达业务语义的能力较弱,从而影响代码的可读性和可理解性。缺乏代码的业务表达能力。想象一下,在程序代码中,你所做的无非是取数据——做计算——存储数据。在这种情况下,如何通过代码明确表达我们的业务呢?老实说,这很难做到,因为我们缺少模型以及它们之间的关系。背离模型的商业表达缺乏节奏和灵魂。比如在上架过程中,有一个检查库存,组合产品(CombineBackOffer)的库存处理会和普通产品不同。原代码是这样写的:booleanisCombineProduct=supplierItem.getSign().isCombProductQuote();//supplier.uscwarehouseneedn'tcheckif(WarehouseTypeEnum.isAliWarehouse(supplierItem.getWarehouseType())){//quotewarehosuecheckif(CollectionUtil.isEmpty(supplIiergetWarehouseIdList())&&!isCombineProduct){throwExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR,"亲,报价无法放行,请联系仓配商建立产品仓关系!");}//inventoryamountcheckLongsellableAmount=0L;if(!isCombineProduct){sellableAmount=normalBiz.acquireSellableAmount(supplierItem.getBackOfferId(),supplierItem.getWarehouseIdList());}else{//设置产品OfferModelbackOffer=backOfferQueryService.getBackOffer(supplierItem.getBackOfferId());if(backOffer!=null){sellableAmount=backOffer.getOffer().getTradeModel().getTradeCondition().getAmountOnSale();}}if(sellableAmount<1){throwExceptionFactory.makeFault(ServiceExceptionCode.SYSTEM_ERROR"亲,真实仓库库存必须大于0才能放行,请确认补货。\r[id:"+supplierItem.getId()+"]");但是,如果我们在系统中引入领域模型,代码会简化为:放行,请联系仓配运营商建立仓位关系!");}if(backOffer.getStockAmount()<1){thrownewBizException("亲,真实仓库存必须大于0才能放行,请确认已经补货了。\r[id:"+backOffer.getSupplierItem().getCspuCode()+"]");}有没有发现使用模型的表达更清晰易懂,没有需要对组合进行判断,因为我们在系统中引入了更符合实际的对象模型(CombineBackOffer继承了BackOffer)。通过对象多态,可以去掉我们代码中大部分的if-else过程分解+对象模型。通过上面的案例可以看出,有流程分解比没有分解好,流程分解+对象模型比只流程分解要好。对于产品listing的案例,如果我们采用流程分解+对象模型的方式,最终会得到一个系统结构如下:如何编写复杂业务代码:自上而下的结构化分解+自下而上的面向对象分析。接下来,让我们进一步提炼上述案例,形成一个实用的方法论,可以泛化到更复杂的业务场景。自上而下的组合所谓自上而下的组合,就是我们需要将自上而下的流程分解和自下而上的对象建模相结合,螺旋式地构建我们的应用系统。这是一个动态过程,两个步骤可以交替进行,也可以同时进行。这两个步骤是互补的。上面的分析可以帮助我们更好的理清模型之间的关系,而下面的模型表达可以提高我们代码的复用性和业务语义的表达能力。流程如下图所示:利用这种顶层和底层的结合,我们可以在面对任何复杂的业务场景时,写出干净、整洁、易维护的代码。能力下沉一般来说,在DDD的实践中有两个过程:概念阶段:理解一些DDD概念,然后在代码中“使用”AggregationRoot、BoundedContext、Repository等概念。此外,还将使用一定的分层策略。但是,这种方法通常不会对复杂性治理产生太大影响。精通阶段:术语不再重要。理解DDD的本质是一种统一语言、划分边界、面向对象分析的方法。总体来说,我大概在1.7的阶段,因为有一个问题一直困扰着我,就是应该把哪些能力放在Domain层,是否按照传统的方式把所有的业务都聚集到Domain上实践,这样是否合理?老实说,我一直没有想清楚这个问题。因为在实际业务中,很多功能都是用例特定的(Usecasespecific),如果你“一味地”使用Domain来聚集业务,可能并不能带来多大的收益。相反,这种收缩会导致Domain层的展开过于厚实,不够纯粹,影响复用性和表现力。鉴于此,我最近的想法是,我们应该采取能力下沉的策略。所谓能力下沉,就是我们不强求一次设计领域的能力,也不需要强行把所有的业务功能都放在领域层,而是采取务实的态度,也就是只有那些将领域层需要在多个场景复用的能力抽象下来,不需要复用的可以暂时放在App层的UseCase中。注意:UseCase是《架构整洁之道》中的一个术语。简单理解就是响应一个Request的过程。通过实践,我发现这种渐进的能力下沉策略应该是一种更现实、更敏捷的方法。因为我们承认模型不是一次设计出来的,而是迭代演进的。下沉过程如下图所示。假设在两个用例中,我们发现uc1的step3和uc2的step1功能相似,我们可以考虑让它们下沉到Domain层,从而增加代码的复用性。指导下沉有两个关键指标:Reusability和cohesionReusability告诉我们When(何时该下沉),也就是有重复代码的时候。内聚是告诉我们How(下沉到哪里),功能是否内聚到合适的实体,是否放在合适的层级(因为Domain层有两个层级的能力,一个是DomainService比较粗——grained,另一个是Domain的Model,就是最细粒度的复用)。比如在我们的产品领域,经常需要判断一个产品是最小单元还是中包产品。直接在Model上挂载这样的能力是非常有必要的。publicclassCSPU{privateStringcode;privateStringbaseCode;//省略其他属性/***为单品最小单位。**/publicbooleanisMinimumUnit(){returnStringUtils.equals(code,baseCode);}/***对中间包的特殊处理**/publicbooleanisMidPackage(){returnStringUtils.equals(code,midPackageCode);}}之前,因为老system中没有域模型,也没有CSPU实体。你会发现判断单个商品是否为最小单元的逻辑以StringUtils.equals(code,baseCode)的形式分散在代码的各个角落。这段代码的可理解性可想而知,至少我第一次看到这段代码的时候,完全不知道是什么意思。业务技术怎么写到这里,想顺便解答一下很多业务技术同学的困惑,也是我之前的困惑:即业务技术是做业务还是做技术?业务技术的技术性在哪里?通过以上案例可以看出,业务面临的复杂度不亚于底层技术,写出好的业务代码并非易事。业务技术和底层技术人员的唯一区别在于,他们面对的问题域不同。业务技术面临的问题域变化更多,面对的人也更复杂。底层技术面临的问题域更稳定,但对技术的要求更深。比如你需要开发Pandora,就需要对Classloader有更深入的了解。但是,不管是业务技术,还是底层技术人员,有些思维和能力是共通的。比如分解问题的能力、抽象思维、结构化思维等等。用我的话说:“如果你不能做好业务开发,你就不能做好底层技术开发,反之亦然。业务开发一点都不简单,但我们很多人把它变得“简单”。因此,从变革的角度来看,业务技术的难度不亚于底层技术,面临的挑战更大。因此,我想对广大从事业务技术开发的同学说:静下心来,巩固自己的基础技术能力、OO能力、建模能力……不断提升抽象思维、结构化思维、思辨思维……继续努力学习,写出好的代码。我们可以在业务技术岗位上做的很“技术”!后记这篇文章是我近期思考的总结。大部分的想法都是继承自我最初写的COLA架构。该架构已经开源,目前在集团内外广泛使用。本文主要在COLA的基础上,针对复杂的业务场景做了进一步的架构实现。个人感觉可以作为COLA的最佳实践。另外,本文讨论的问题的大小与文章的篇幅不成正比。原因是我假设您已经了解DDD和应用程序架构的一些基础知识。如果你觉得难以理解,我建议你先阅读《领域驱动设计》和《架构整洁之道》这本书。如果你没有那么多时间,也可以快速浏览一下我之前的两篇文章应用架构与领域建模之道,了解我之前的一些想法。