阿里妹導讀:張建飛是阿里巴巴高級技術專家,一直在致力於應用架構和代碼複雜度的治理。最近,他在看零售通商品域的代碼。面對零售通如此複雜的業務場景,如何在架構和代碼層面進行應對,是一個新課題。結合實際的業務場景,Frank 沉澱了一套「如何寫複雜業務代碼」的方法論,在此分享給你們,相信一樣的方法論能夠複製到大部分複雜業務場景。
業務背景數據庫
簡單的介紹下業務背景,零售通是給線下小店供貨的B2B模式,咱們但願經過數字化重構傳統供應鏈渠道,提高供應鏈效率,爲新零售助力。阿里在中間是一個平臺角色,提供的是Bsbc中的service的功能。後端
商品力是零售通的核心所在,一個商品在零售通的生命週期以下圖所示:設計模式
在上圖中紅框標識的是一個運營操做的「上架」動做,這是很是關鍵的業務操做。上架以後,商品就能在零售通上面對小店進行銷售了。由於上架操做很是關鍵,因此也是商品域中最複雜的業務之一,涉及不少的數據校驗和關聯操做。
針對上架,一個簡化的業務流程以下所示:架構
過程分解ide
像這麼複雜的業務,我想應該沒有人會寫在一個service方法中吧。一個類解決不了,那就分治吧。工具
說實話,能想到分而治之的工程師,已經作的不錯了,至少比沒有分治思惟要好不少。我也見過複雜程度至關的業務,連分解都沒有,就是一堆方法和類的堆砌。學習
不過,這裏存在一個問題:即不少同窗過分的依賴工具或是輔助手段來實現分解。好比在咱們的商品域中,相似的分解手段至少有3套以上,有自制的流程引擎,有依賴於數據庫配置的流程處理:ui
本質上來說,這些輔助手段作的都是一個pipeline的處理流程,沒有其它。所以,我建議此處最好保持KISS(Keep It Simple and Stupid),即最好是什麼工具都不要用,次之是用一個極簡的Pipeline模式,最差是使用像流程引擎這樣的重方法。spa
除非你的應用有極強的流程可視化和編排的訴求,不然我很是不推薦使用流程引擎等工具。第一,它會引入額外的複雜度,特別是那些須要持久化狀態的流程引擎;第二,它會割裂代碼,致使閱讀代碼的不暢。大膽斷言一下,全天下估計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)上。
過程分解後的兩個問題
的確,使用過程分解以後的代碼,已經比之前的代碼更清晰、更容易維護了。不過,還有兩個問題值得咱們去關注一下:
什麼叫被肢解?由於咱們到目前爲止作的都是過程化拆解,致使沒有一個聚合領域知識的地方。每一個Use Case的代碼只關心本身的處理流程,知識沒有沉澱。
相同的業務邏輯會在多個Use Case中被重複實現,致使代碼重複度高,即便有複用,最多也就是抽取一個util,代碼對業務語義的表達能力很弱,從而影響代碼的可讀性和可理解性。
試想下,在過程式的代碼中,所作的事情無外乎就是取數據--作計算--存數據,在這種狀況下,要如何經過代碼顯性化的表達咱們的業務呢?說實話,很難作到,由於咱們缺失了模型,以及模型之間的關係。脫離模型的業務表達,是缺乏韻律和靈魂的。
舉個例子,在上架過程當中,有一個校驗是檢查庫存的,其中對於組合品(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() + "]"); } }
然而,若是咱們在系統中引入領域模型以後,其代碼會簡化爲以下:
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有兩個過程:
套概念階段:瞭解了一些DDD的概念,而後在代碼中「使用」Aggregation Root,Bounded Context,Repository等等這些概念。更進一步,也會使用必定的分層策略。然而這種作法通常對複雜度的治理並無多大做用。
融會貫通階段:術語已經再也不重要,理解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能力、建模能力... 不斷提高抽象思惟、結構化思惟、思辨思惟... 持續學習精進,寫好代碼。咱們能夠在業務技術崗作的很」技術「!。
這篇文章是我最近思考的一些總結,大部分思想是繼承自我原來寫的COLA架構,該架構已經開源,目前在集團內外都有比較普遍的使用。
這一篇主要是在COLA的基礎上,針對複雜業務場景,作了進一步的架構落地。我的感受能夠做爲COLA的最佳實踐來使用。
另外,本文討論的問題之大和篇幅之短是不成正比的。緣由是我假定你已經瞭解了一些DDD和應用架構的基礎知識。若是以爲在理解上有困難,我建議能夠先看下《領域驅動設計》和《架構整潔之道》這兩本書。
若是沒有那麼多時間,也能夠快速瀏覽下我以前的兩篇文章應用架構之道 和 領域建模去知曉一下我以前的思想脈絡。
本文來自雲棲社區合做夥伴「阿里技術」,如需轉載請聯繫原做者。