阿裡高級技術專家方法論:如何寫複雜業務代碼?

来源:https://www.cnblogs.com/88223100/archive/2023/01/28/How-to-write-complex-business-code.html
-Advertisement-
Play Games

張建飛是阿裡巴巴高級技術專家,一直在致力於應用架構和代碼複雜度的治理。最近,他在看零售通商品域的代碼。面對零售通如此複雜的業務場景,如何在架構和代碼層面進行應對,是一個新課題。結合實際的業務場景,他沉澱了一套“如何寫複雜業務代碼”的方法論,在此分享給大家,相信同樣的方法論可以複製到大部分複雜業務場景... ...


 

張建飛是阿裡巴巴高級技術專家,一直在致力於應用架構和代碼複雜度的治理。最近,他在看零售通商品域的代碼。面對零售通如此複雜的業務場景,如何在架構和代碼層面進行應對,是一個新課題。結合實際的業務場景,他沉澱了一套“如何寫複雜業務代碼”的方法論,在此分享給大家,相信同樣的方法論可以複製到大部分複雜業務場景。

 

 1 
一個複雜業務的處理過程

 

業務背景

簡單的介紹下業務背景,零售通是給線下小店供貨的B2B模式,我們希望通過數字化重構傳統供應鏈渠道,提升供應鏈效率,為新零售助力。阿裡在中間是一個平臺角色,提供的是Bsbc中的service的功能。

圖片

 

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

圖片

 

在上圖中紅框標識的是一個運營操作的“上架”動作,這是非常關鍵的業務操作。上架之後,商品就能在零售通上面對小店進行銷售了。因為上架操作非常關鍵,所以也是商品域中最複雜的業務之一,涉及很多的數據校驗和關聯操作。

針對上架,一個簡化的業務流程如下所示:

圖片

 

過程分解

像這麼複雜的業務,我想應該沒有人會寫在一個service方法中吧。一個類解決不了,那就分治吧。

說實話,能想到分而治之的工程師,已經做的不錯了,至少比沒有分治思維要好很多。我也見過複雜程度相當的業務,連分解都沒有,就是一堆方法和類的堆砌。

不過,這裡存在一個問題:即很多同學過度的依賴工具或是輔助手段來實現分解。比如在我們的商品域中,類似的分解手段至少有3套以上,有自製的流程引擎,有依賴於資料庫配置的流程處理:

圖片

 

本質上來講,這些輔助手段做的都是一個pipeline的處理流程,沒有其它。因此,我建議此處最好保持KISS(Keep It Simple and Stupid),即最好是什麼工具都不要用,次之是用一個極簡的Pipeline模式,最差是使用像流程引擎這樣的重方法。

除非你的應用有極強的流程可視化和編排的訴求,否則我非常不推薦使用流程引擎等工具。第一,它會引入額外的複雜度,特別是那些需要持久化狀態的流程引擎;第二,它會割裂代碼,導致閱讀代碼的不順暢。大膽斷言一下,全天下估計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)上。

圖片

過程分解後的兩個問題

的確,使用過程分解之後的代碼,已經比以前的代碼更清晰、更容易維護了。不過,還有兩個問題值得我們去關註一下:

1、領域知識被割裂肢解

什麼叫被肢解?因為我們到目前為止做的都是過程化拆解,導致沒有一個聚合領域知識的地方。每個Use Case的代碼只關心自己的處理流程,知識沒有沉澱。

相同的業務邏輯會在多個Use Case中被重覆實現,導致代碼重覆度高,即使有復用,最多也就是抽取一個util,代碼對業務語義的表達能力很弱,從而影響代碼的可讀性和可理解性。

2、代碼的業務表達能力缺失

試想下,在過程式的代碼中,所做的事情無外乎就是取數據--做計算--存數據,在這種情況下,要如何通過代碼顯性化的表達我們的業務呢?說實話,很難做到,因為我們缺失了模型,以及模型之間的關係。脫離模型的業務表達,是缺少韻律和靈魂的。

舉個例子,在上架過程中,有一個校驗是檢查庫存的,其中對於組合品(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() + "]");
}
}
然而,如果我們在系統中引入領域模型之後,其代碼會簡化為如下:
if(backOffer.isCloudWarehouse()){
    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,如果採用過程分解+對象模型的方式,最終我們會得到一個如下的系統結構:

圖片

 

 2 
寫複雜業務的方法論

 

通過上面案例的講解,我想說,我已經交代了複雜業務代碼要怎麼寫:即自上而下的結構化分解+自下而上的面向對象分析。

接下來,讓我們把上面的案例進行進一步的提煉,形成一個可落地的方法論,從而可以泛化到更多的複雜業務場景。

上下結合

所謂上下結合,是指我們要結合自上而下的過程分解和自下而上的對象建模,螺旋式的構建我們的應用系統。這是一個動態的過程,兩個步驟可以交替進行、也可以同時進行。

這兩個步驟是相輔相成的,上面的分析可以幫助我們更好的理清模型之間的關係,而下麵的模型表達可以提升我們代碼的復用度和業務語義表達能力。

其過程如下圖所示:

圖片

使用這種上下結合的方式,我們就有可能在面對任何複雜的業務場景,都能寫出乾凈整潔、易維護的代碼。

能力下沉

一般來說實踐DDD有兩個過程:

1、套概念階段:瞭解了一些DDD的概念,然後在代碼中“使用”Aggregation Root,Bounded Context,Repository等等這些概念。更進一步,也會使用一定的分層策略。然而這種做法一般對複雜度的治理並沒有多大作用。

2、融會貫通階段:術語已經不再重要,理解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)的形式散落在代碼的各個角落。這種代碼的可理解性是可想而知的,至少我在第一眼看到這個代碼的時候,是完全不知道什麼意思。

 

 3 
業務技術要怎麼做

 

寫到這裡,我想順便回答一下很多業務技術同學的困惑,也是我之前的困惑:即業務技術到底是在做業務,還是做技術?業務技術的技術性體現在哪裡?

通過上面的案例,我們可以看到業務所面臨的複雜性並不亞於底層技術,要想寫好業務代碼也不是一件容易的事情。業務技術和底層技術人員唯一的區別是他們所面臨的問題域不一樣。

業務技術面對的問題域變化更多、面對的人更加龐雜。而底層技術面對的問題域更加穩定、但對技術的要求更加深。比如,如果你需要去開發Pandora,你就要對Classloader有更加深入的瞭解才行。

但是,不管是業務技術還是底層技術人員,有一些思維和能力都是共通的。比如,分解問題的能力,抽象思維,結構化思維等等。

圖片

用我的話說就是:“做不好業務開發的,也做不好技術底層開發,反之亦然。業務開發一點都不簡單,只是我們很多人把它做‘簡單’了”。

因此,如果從變化的角度來看,業務技術的難度一點不遜色於底層技術,其面臨的挑戰甚至更大。因此,我想對廣大的從事業務技術開發的同學說:沉下心來,夯實自己的基礎技術能力、OO能力、建模能力……不斷提升抽象思維、結構化思維、思辨思維……持續學習精進,寫好代碼。我們可以在業務技術崗做的很“技術”!。

 

 4 
後記

 

這篇文章是我最近思考的一些總結,大部分思想是繼承自我原來寫的COLA架構,該架構已經開源,目前在集團內外都有比較廣泛的使用。

這一篇主要是在COLA的基礎上,針對複雜業務場景,做了進一步的架構落地。個人感覺可以作為COLA的最佳實踐來使用。

另外,本文討論的問題之大和篇幅之短是不成正比的。原因是我假定你已經瞭解了一些DDD和應用架構的基礎知識。如果覺得在理解上有困難,我建議可以先看下《領域驅動設計》和《架構整潔之道》這兩本書。

 

作者|張建飛

本文來自博客園,作者:古道輕風,轉載請註明原文鏈接:https://www.cnblogs.com/88223100/p/How-to-write-complex-business-code.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 痞子衡單片機排行榜(2022Q4) 繼2020年開辦的《痞子衡嵌入式半月刊》之後,從2023年1月份開始痞子衡將為大家帶來新項目《痞子衡單片機排行榜》(一年分四季,每個季度發佈一期),以MCU主頻和Coremark跑分為基礎(後期會加入更多指標),搜羅國內外8051/ARM/RISC-V等不同賽道的 ...
  • 固件更新 需將小米AX9000路由支持安裝Docker,但正式版並沒有該功能需更新為開發板Rom,直接在小米路由官網下載固件更新即可,當前正式版最新固件為1.0.165,開發板固件為:1.0.140; 固件更新為140開發版本後即可在小米路由管理頁面的高級設置項中看到DOCKER的選項; 安裝Doc ...
  • 一:背景 1. 講故事 大家都知道資料庫應用程式 它天生需要圍繞著數據文件打轉,諸如包含數據的 .mdf,事務日誌的 .ldf,很多時候深入瞭解這兩類文件的合成原理,差不多對資料庫就能理解一半了,關於 .mdf 的合成前面的文章已經有所介紹,這篇我們來聊一下 .ldf 的一些內部知識,比如 LSN。 ...
  • 本文基於此: Flutter中文網 一、安裝和運行Flutter的系統環境要求 想要安裝並運行 Flutter,你的開發環境需要最低滿足以下要求: 操作系統:macOS 磁碟空間:2.8 GB(不包括IDE/tools的磁碟空間)。 工具:Flutter使用git進行安裝和升級。我們建議安裝Xcod ...
  • 主題說明 打開博客園的隨筆詳細頁、標簽頁等,都是整頁重新載入,比較影響體驗。SPA 應用可以減少整頁載入,實現局部刷新,本皮膚通過 Vue3 + TS + Vite 開發的。有些細節待日後逐步完善,隨筆的閱讀和使用基本上沒有問題,文章、日記、部分側邊欄內容還沒有實現。 倉庫地址:GitHub,請點個 ...
  • 值和類型 八種數據類型 undefined、null、boolean、number、string、symbol、bigint、object 原始值和引用值 原始值:按值訪問。值存儲在棧中。變數賦值傳遞時會創建該值的副本,兩個變數(複製與被覆制)完全獨立。 常見原始值類型:undefined、null ...
  • 參考:https://www.cnblogs.com/lxlx1798/articles/16969244.html 要麼使用流讀取器,要麼使用 Reponse 的方法來獲取結果,不能同時使用兩種方法來讀取相同的響應。 直接獲取: Response.blob() 方法返回一個 resolve 返回值 ...
  • 說起轉義字元,大家最先想到的肯定是使用反斜杠,這也是我們最常見的,很多編程語言都支持。 轉義字元從字面上講,就是能夠轉變字元原本的意義,得到新的字元。常用在特殊字元的顯示以及特定的編碼環境中。 除了反斜杠以外,在前端開發中,還有其他幾種轉義字元,也是較常見的,本文將對這些做一個總結。 字元串中的轉義 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...