阿里高級(jí)技術(shù)專家方法論:如何寫復(fù)雜業(yè)務(wù)代碼?
張建飛是阿里巴巴高級(jí)技術(shù)專家,一直在致力于應(yīng)用架構(gòu)和代碼復(fù)雜度的治理。最近,他在看零售通商品域的代碼。面對(duì)零售通如此復(fù)雜的業(yè)務(wù)場(chǎng)景,如何在架構(gòu)和代碼層面進(jìn)行應(yīng)對(duì),是一個(gè)新課題。結(jié)合實(shí)際的業(yè)務(wù)場(chǎng)景,F(xiàn)rank 沉淀了一套“如何寫復(fù)雜業(yè)務(wù)代碼”的方法論,在此分享給大家,相信同樣的方法論可以復(fù)制到大部分復(fù)雜業(yè)務(wù)場(chǎng)景。
一個(gè)復(fù)雜業(yè)務(wù)的處理過程
業(yè)務(wù)背景
簡(jiǎn)單的介紹下業(yè)務(wù)背景,零售通是給線下小店供貨的B2B模式,我們希望通過數(shù)字化重構(gòu)傳統(tǒng)供應(yīng)鏈渠道,提升供應(yīng)鏈效率,為新零售助力。阿里在中間是一個(gè)平臺(tái)角色,提供的是Bsbc中的service的功能。
商品力是零售通的核心所在,一個(gè)商品在零售通的生命周期如下圖所示:
在上圖中紅框標(biāo)識(shí)的是一個(gè)運(yùn)營(yíng)操作的“上架”動(dòng)作,這是非常關(guān)鍵的業(yè)務(wù)操作。上架之后,商品就能在零售通上面對(duì)小店進(jìn)行銷售了。因?yàn)樯霞懿僮鞣浅jP(guān)鍵,所以也是商品域中最復(fù)雜的業(yè)務(wù)之一,涉及很多的數(shù)據(jù)校驗(yàn)和關(guān)聯(lián)操作。針對(duì)上架,一個(gè)簡(jiǎn)化的業(yè)務(wù)流程如下所示:
過程分解
像這么復(fù)雜的業(yè)務(wù),我想應(yīng)該沒有人會(huì)寫在一個(gè)service方法中吧。一個(gè)類解決不了,那就分治吧。
說實(shí)話,能想到分而治之的工程師,已經(jīng)做的不錯(cuò)了,至少比沒有分治思維要好很多。我也見過復(fù)雜程度相當(dāng)?shù)臉I(yè)務(wù),連分解都沒有,就是一堆方法和類的堆砌。
不過,這里存在一個(gè)問題:即很多同學(xué)過度的依賴工具或是輔助手段來(lái)實(shí)現(xiàn)分解。比如在我們的商品域中,類似的分解手段至少有3套以上,有自制的流程引擎,有依賴于數(shù)據(jù)庫(kù)配置的流程處理:
本質(zhì)上來(lái)講,這些輔助手段做的都是一個(gè)pipeline的處理流程,沒有其它。因此,我建議此處最好保持KISS(Keep It Simple and Stupid),即最好是什么工具都不要用,次之是用一個(gè)極簡(jiǎn)的Pipeline模式,最差是使用像流程引擎這樣的重方法。
除非你的應(yīng)用有極強(qiáng)的流程可視化和編排的訴求,否則我非常不推薦使用流程引擎等工具。第一,它會(huì)引入額外的復(fù)雜度,特別是那些需要持久化狀態(tài)的流程引擎;第二,它會(huì)割裂代碼,導(dǎo)致閱讀代碼的不順暢。大膽斷言一下,全天下估計(jì)80%對(duì)流程引擎的使用都是得不償失的。
回到商品上架的問題,這里問題核心是工具嗎?是設(shè)計(jì)模式帶來(lái)的代碼靈活性嗎?顯然不是,問題的核心應(yīng)該是如何分解問題和抽象問題,知道金字塔原理的應(yīng)該知道,此處,我們可以使用結(jié)構(gòu)化分解將問題解構(gòu)成一個(gè)有層級(jí)的金字塔結(jié)構(gòu):
按照這種分解寫的代碼,就像一本書,目錄和內(nèi)容清晰明了。
以商品上架為例,程序的入口是一個(gè)上架命令(OnSaleCommand), 它由三個(gè)階段(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);
- }
- }
每個(gè)Phase又可以拆解成多個(gè)步驟(Step),以O(shè)nSaleProcessPhase為例,它是由一系列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);
- // 發(fā)布商品
- publishOffer(supplierItem);
- // 前后端庫(kù)存綁定 backoffer域
- bindBackOfferStock(supplierItem);
- // 同步庫(kù)存路由 backoffer域
- syncStockRoute(supplierItem);
- // 設(shè)置虛擬商品拓展字段
- setVirtualProductExtension(supplierItem);
- // 發(fā)貨保障打標(biāo) offer域
- markSendProtection(supplierItem);
- // 記錄變更內(nèi)容ChangeDetail
- recordChangeDetail(supplierItem);
- // 同步供貨價(jià)到BackOffer
- syncSupplyPriceToBackOffer(supplierItem);
- // 如果是組合商品打標(biāo),寫擴(kuò)展信息
- setCombineProductExtension(supplierItem);
- // 去售罄標(biāo)
- removeSellOutTag(offerId);
- // 發(fā)送領(lǐng)域事件
- fireDomainEvent(supplierItem);
- // 關(guān)閉關(guān)聯(lián)的待辦事項(xiàng)
- closeIssues(supplierItem);
- }
- }
看到了嗎,這就是商品上架這個(gè)復(fù)雜業(yè)務(wù)的業(yè)務(wù)流程。需要流程引擎嗎?不需要,需要設(shè)計(jì)模式支撐嗎?也不需要。對(duì)于這種業(yè)務(wù)流程的表達(dá),簡(jiǎn)單樸素的組合方法模式(Composed Method)是再合適不過的了。
因此,在做過程分解的時(shí)候,我建議工程師不要把太多精力放在工具上,放在設(shè)計(jì)模式帶來(lái)的靈活性上。而是應(yīng)該多花時(shí)間在對(duì)問題分析,結(jié)構(gòu)化分解,最后通過合理的抽象,形成合適的階段(Phase)和步驟(Step)上。
過程分解后的兩個(gè)問題
的確,使用過程分解之后的代碼,已經(jīng)比以前的代碼更清晰、更容易維護(hù)了。不過,還有兩個(gè)問題值得我們?nèi)リP(guān)注一下:
- 領(lǐng)域知識(shí)被割裂肢解
什么叫被肢解?因?yàn)槲覀兊侥壳盀橹棺龅亩际沁^程化拆解,導(dǎo)致沒有一個(gè)聚合領(lǐng)域知識(shí)的地方。每個(gè)Use Case的代碼只關(guān)心自己的處理流程,知識(shí)沒有沉淀。
相同的業(yè)務(wù)邏輯會(huì)在多個(gè)Use Case中被重復(fù)實(shí)現(xiàn),導(dǎo)致代碼重復(fù)度高,即使有復(fù)用,最多也就是抽取一個(gè)util,代碼對(duì)業(yè)務(wù)語(yǔ)義的表達(dá)能力很弱,從而影響代碼的可讀性和可理解性。
- 代碼的業(yè)務(wù)表達(dá)能力缺失
試想下,在過程式的代碼中,所做的事情無(wú)外乎就是取數(shù)據(jù)--做計(jì)算--存數(shù)據(jù),在這種情況下,要如何通過代碼顯性化的表達(dá)我們的業(yè)務(wù)呢?說實(shí)話,很難做到,因?yàn)槲覀內(nèi)笔Я四P?,以及模型之間的關(guān)系。脫離模型的業(yè)務(wù)表達(dá),是缺少韻律和靈魂的。
舉個(gè)例子,在上架過程中,有一個(gè)校驗(yàn)是檢查庫(kù)存的,其中對(duì)于組合品(CombineBackOffer)其庫(kù)存的處理會(huì)和普通品不一樣。原來(lái)的代碼是這么寫的:
- 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, "親,不能發(fā)布Offer,請(qǐng)聯(lián)系倉(cāng)配運(yùn)營(yíng)人員,建立品倉(cāng)關(guān)系!");
- }
- // 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, "親,實(shí)倉(cāng)庫(kù)存必須大于0才能發(fā)布,請(qǐng)確認(rèn)已補(bǔ)貨.\r[id:" + supplierItem.getId() + "]");
- }
- }
然而,如果我們?cè)谙到y(tǒng)中引入領(lǐng)域模型之后,其代碼會(huì)簡(jiǎn)化為如下:
- if(backOffer.isCloudWarehouse()){
- return;
- }
- if (backOffer.isNonInWarehouse()){
- throw new BizException("親,不能發(fā)布Offer,請(qǐng)聯(lián)系倉(cāng)配運(yùn)營(yíng)人員,建立品倉(cāng)關(guān)系!");
- }
- if (backOffer.getStockAmount() < 1){
- throw new BizException("親,實(shí)倉(cāng)庫(kù)存必須大于0才能發(fā)布,請(qǐng)確認(rèn)已補(bǔ)貨.\r[id:" + backOffer.getSupplierItem().getCspuCode() + "]");
- }
有沒有發(fā)現(xiàn),使用模型的表達(dá)要清晰易懂很多,而且也不需要做關(guān)于組合品的判斷了,因?yàn)槲覀冊(cè)谙到y(tǒng)中引入了更加貼近現(xiàn)實(shí)的對(duì)象模型(CombineBackOffer繼承BackOffer),通過對(duì)象的多態(tài)可以消除我們代碼中的大部分的if-else。
過程分解+對(duì)象模型
通過上面的案例,我們可以看到有過程分解要好于沒有分解,過程分解+對(duì)象模型要好于僅僅是過程分解。對(duì)于商品上架這個(gè)case,如果采用過程分解+對(duì)象模型的方式,最終我們會(huì)得到一個(gè)如下的系統(tǒng)結(jié)構(gòu):
寫復(fù)雜業(yè)務(wù)的方法論
通過上面案例的講解,我想說,我已經(jīng)交代了復(fù)雜業(yè)務(wù)代碼要怎么寫:即自上而下的結(jié)構(gòu)化分解+自下而上的面向?qū)ο蠓治觥?/p>
接下來(lái),讓我們把上面的案例進(jìn)行進(jìn)一步的提煉,形成一個(gè)可落地的方法論,從而可以泛化到更多的復(fù)雜業(yè)務(wù)場(chǎng)景。
上下結(jié)合
所謂上下結(jié)合,是指我們要結(jié)合自上而下的過程分解和自下而上的對(duì)象建模,螺旋式的構(gòu)建我們的應(yīng)用系統(tǒng)。這是一個(gè)動(dòng)態(tài)的過程,兩個(gè)步驟可以交替進(jìn)行、也可以同時(shí)進(jìn)行。
這兩個(gè)步驟是相輔相成的,上面的分析可以幫助我們更好的理清模型之間的關(guān)系,而下面的模型表達(dá)可以提升我們代碼的復(fù)用度和業(yè)務(wù)語(yǔ)義表達(dá)能力。
其過程如下圖所示:
使用這種上下結(jié)合的方式,我們就有可能在面對(duì)任何復(fù)雜的業(yè)務(wù)場(chǎng)景,都能寫出干凈整潔、易維護(hù)的代碼。
能力下沉
一般來(lái)說實(shí)踐DDD有兩個(gè)過程:
- 套概念階段:了解了一些DDD的概念,然后在代碼中“使用”Aggregation Root,Bounded Context,Repository等等這些概念。更進(jìn)一步,也會(huì)使用一定的分層策略。然而這種做法一般對(duì)復(fù)雜度的治理并沒有多大作用。
- 融會(huì)貫通階段:術(shù)語(yǔ)已經(jīng)不再重要,理解DDD的本質(zhì)是統(tǒng)一語(yǔ)言、邊界劃分和面向?qū)ο蠓治龅姆椒ā?/li>
大體上而言,我大概是在1.7的階段,因?yàn)橛幸粋€(gè)問題一直在困擾我,就是哪些能力應(yīng)該放在Domain層,是不是按照傳統(tǒng)的做法,將所有的業(yè)務(wù)都收攏到Domain上,這樣做合理嗎?說實(shí)話,這個(gè)問題我一直沒有想清楚。
因?yàn)樵诂F(xiàn)實(shí)業(yè)務(wù)中,很多的功能都是用例特有的(Use case specific)的,如果“盲目”的使用Domain收攏業(yè)務(wù)并不見得能帶來(lái)多大的益處。相反,這種收攏會(huì)導(dǎo)致Domain層的膨脹過厚,不夠純粹,反而會(huì)影響復(fù)用性和表達(dá)能力。
鑒于此,我最近的思考是我們應(yīng)該采用能力下沉的策略。
所謂的能力下沉,是指我們不強(qiáng)求一次就能設(shè)計(jì)出Domain的能力,也不需要強(qiáng)制要求把所有的業(yè)務(wù)功能都放到Domain層,而是采用實(shí)用主義的態(tài)度,即只對(duì)那些需要在多個(gè)場(chǎng)景中需要被復(fù)用的能力進(jìn)行抽象下沉,而不需要復(fù)用的,就暫時(shí)放在App層的Use Case里就好了。
注:Use Case是《架構(gòu)整潔之道》里面的術(shù)語(yǔ),簡(jiǎn)單理解就是響應(yīng)一個(gè)Request的處理過程。
通過實(shí)踐,我發(fā)現(xiàn)這種循序漸進(jìn)的能力下沉策略,應(yīng)該是一種更符合實(shí)際、更敏捷的方法。因?yàn)槲覀兂姓J(rèn)模型不是一次性設(shè)計(jì)出來(lái)的,而是迭代演化出來(lái)的。
下沉的過程如下圖所示,假設(shè)兩個(gè)use case中,我們發(fā)現(xiàn)uc1的step3和uc2的step1有類似的功能,我們就可以考慮讓其下沉到Domain層,從而增加代碼的復(fù)用性。
指導(dǎo)下沉有兩個(gè)關(guān)鍵指標(biāo):
- 復(fù)用性
- 內(nèi)聚性
復(fù)用性是告訴我們When(什么時(shí)候該下沉了),即有重復(fù)代碼的時(shí)候。內(nèi)聚性是告訴我們How(要下沉到哪里),功能有沒有內(nèi)聚到恰當(dāng)?shù)膶?shí)體上,有沒有放到合適的層次上(因?yàn)镈omain層的能力也是有兩個(gè)層次的,一個(gè)是Domain Service這是相對(duì)比較粗的粒度,另一個(gè)是Domain的Model這個(gè)是最細(xì)粒度的復(fù)用)。
比如,在我們的商品域,經(jīng)常需要判斷一個(gè)商品是不是最小單位,是不是中包商品。像這種能力就非常有必要直接掛載在Model上。
- public class CSPU {
- private String code;
- private String baseCode;
- //省略其它屬性
- /**
- * 單品是否為最小單位。
- *
- */
- public boolean isMinimumUnit(){
- return StringUtils.equals(code, baseCode);
- }
- /**
- * 針對(duì)中包的特殊處理
- *
- */
- public boolean isMidPackage(){
- return StringUtils.equals(code, midPackageCode);
- }
- }
之前,因?yàn)槔舷到y(tǒng)中沒有領(lǐng)域模型,沒有CSPU這個(gè)實(shí)體。你會(huì)發(fā)現(xiàn)像判斷單品是否為最小單位的邏輯是以StringUtils.equals(code, baseCode)的形式散落在代碼的各個(gè)角落。這種代碼的可理解性是可想而知的,至少我在第一眼看到這個(gè)代碼的時(shí)候,是完全不知道什么意思。
業(yè)務(wù)技術(shù)要怎么做
寫到這里,我想順便回答一下很多業(yè)務(wù)技術(shù)同學(xué)的困惑,也是我之前的困惑:即業(yè)務(wù)技術(shù)到底是在做業(yè)務(wù),還是做技術(shù)?業(yè)務(wù)技術(shù)的技術(shù)性體現(xiàn)在哪里?
通過上面的案例,我們可以看到業(yè)務(wù)所面臨的復(fù)雜性并不亞于底層技術(shù),要想寫好業(yè)務(wù)代碼也不是一件容易的事情。業(yè)務(wù)技術(shù)和底層技術(shù)人員唯一的區(qū)別是他們所面臨的問題域不一樣。
業(yè)務(wù)技術(shù)面對(duì)的問題域變化更多、面對(duì)的人更加龐雜。而底層技術(shù)面對(duì)的問題域更加穩(wěn)定、但對(duì)技術(shù)的要求更加深。比如,如果你需要去開發(fā)Pandora,你就要對(duì)Classloader有更加深入的了解才行。
但是,不管是業(yè)務(wù)技術(shù)還是底層技術(shù)人員,有一些思維和能力都是共通的。比如,分解問題的能力,抽象思維,結(jié)構(gòu)化思維等等。
用我的話說就是:“做不好業(yè)務(wù)開發(fā)的,也做不好技術(shù)底層開發(fā),反之亦然。業(yè)務(wù)開發(fā)一點(diǎn)都不簡(jiǎn)單,只是我們很多人把它做“簡(jiǎn)單”了。
因此,如果從變化的角度來(lái)看,業(yè)務(wù)技術(shù)的難度一點(diǎn)不遜色于底層技術(shù),其面臨的挑戰(zhàn)甚至更大。因此,我想對(duì)廣大的從事業(yè)務(wù)技術(shù)開發(fā)的同學(xué)說:沉下心來(lái),夯實(shí)自己的基礎(chǔ)技術(shù)能力、OO能力、建模能力... 不斷提升抽象思維、結(jié)構(gòu)化思維、思辨思維... 持續(xù)學(xué)習(xí)精進(jìn),寫好代碼。我們可以在業(yè)務(wù)技術(shù)崗做的很”技術(shù)“!。
后記
這篇文章是我最近思考的一些總結(jié),大部分思想是繼承自我原來(lái)寫的COLA架構(gòu),該架構(gòu)已經(jīng)開源,目前在集團(tuán)內(nèi)外都有比較廣泛的使用。
這一篇主要是在COLA的基礎(chǔ)上,針對(duì)復(fù)雜業(yè)務(wù)場(chǎng)景,做了進(jìn)一步的架構(gòu)落地。個(gè)人感覺可以作為COLA的最佳實(shí)踐來(lái)使用。
另外,本文討論的問題之大和篇幅之短是不成正比的。原因是我假定你已經(jīng)了解了一些DDD和應(yīng)用架構(gòu)的基礎(chǔ)知識(shí)。如果覺得在理解上有困難,我建議可以先看下《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》和《架構(gòu)整潔之道》這兩本書。
如果沒有那么多時(shí)間,也可以快速瀏覽下我之前的兩篇文章應(yīng)用架構(gòu)之道 和 領(lǐng)域建模去知曉一下我之前的思想脈絡(luò)。