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






































