當前位置: 妍妍網 > 碼農

當中台過氣,微服務回歸單體,DDD的意義何在?

2024-06-27碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

👉 目錄

1 領域驅動的理念

2 那些晦澀難懂的術語

3 拆分與合並

4 總結

2015年之後,隨著雲原生、微服務、大中台等一系列技術名詞誕生的同時,還有一個耳熟能詳的名詞「領域驅動」也開始被捧上神壇。筆者初次聽到領域驅動是參加一個技術分享會,當時給我的直觀感受就是:好像說了什麽,但又好像什麽都沒說,很多概念很"形而上學",在天空中飄啊飄,無法落地。

十年過去了,中台已經過氣,微服務回歸單體也一度成為技術圈討論的熱點話題,曾經神壇上雲遮霧繞的 DDD 在今天看來是否還有討論的意義?在過去一兩年的實踐中,筆者對 DDD 有了更深的體會,本文將闡述我的一些淺見,如果有理解不到位的地方,也希望同學們一起討論。

領域驅動的理念

領域驅動這個概念一開始是由大神 Eric Evans 在2003釋出他的名著【Domain Driven Design:Tackling the Complexity in the Heart of Software】中提出,從標題中可以直觀的知道 DDD 是為了解決軟體系統的復雜性問題,是一種降低業務系統復雜度的方法論,許多同學認為領域驅動難以理解,是因為他提出了很多抽象的概念,我們不妨先拋開這些概念,先去理解一下它去解決問題的思路。

1.1 統一語言與模型

對於一個開發來說,我們的工作一句話就是:用程式碼實作需求,在實作的過程中不同的人、不同的團隊,可以有不同的實踐,領域驅動就是其中的一種實作路徑。領域驅動在需求到程式碼之間試圖建立起一條橋梁,橋梁的名字 叫統一語言和模型。

什麽是統一語言?軟體開發的 核心難度就在於處理隱藏在業務知識中的復雜度, 想要處理這種復雜度,首先需要打破 業務與技術之間溝通壁壘, 在一個計畫中,不光是有開發人員,還有測試、運維、產品、pm 等等,能把事做成的前提是可以把事情說清楚,我們知道中華文化博大精深一句話在不同的環境、場合下完全有不同的語意,統一語言的思想就是提倡團隊內透過不斷溝通去確定在一個業務領域中的術語或概念有唯一明確的語意。

那什麽是模型呢?我總結為: 一種化繁就簡的抽象 ,抽象是目的是為了簡化問題, 先忽略細節,從頂層思考問題, 抽象不在乎形式的表達,而取決於如何看待問題和分析問題的角度,我們舉幾個例子說明:

  • 把大象裝進冰箱需要幾步?一共三步,開啟冰箱->放入大象->關上冰箱。

  • 雖然這是一個梗,但不得不說,這是很好的一種 程序導向的抽象。

  • 程式=數據結構+演算法。

  • 這已經變成了電腦科學中最基本的一條原則,即任何程式都可以分解為演算法+數據結構,雖然程式要解決的問題都沒有確定,但是已經有了一個思考問題的方向。

  • 類圖、流程圖、架構圖...

  • 對於一個比較復雜的系統,我們很難透過幾句話把它講清楚,這個時候,畫圖就成為了一個好的表達方式,而畫圖就是一種抽象,根據你想抽象角度的不同使用不同型別的圖。例如有人讓你講講購物網站做了什麽,你可以把問題簡化並用下圖表示,即描述使用者、商家、平台之間的關系,這正是物件導向的建模思路,而 領域驅動本質上也是一種物件導向的建模方法。

    在引入統一語言和模型抽象的思路之後,就可以把需求到實作的這個過程用下圖表示,技術和業務的相關同學透過統一語言去溝通交流需求,透過模型抽象描述需求,最後按照模型去實作相應的程式碼,領域驅動的一大目標是:修改需求即修改統一語言,修改統一語言即修改模型,修改模型即修改程式碼,這也就實作了從需求到程式碼的有效資訊傳遞。

    1.2 分而治之:再談歸並排序

    為什麽這裏要談起歸並演算法,因為領域驅動所提倡解決問題的思維方式和歸並排序演算法如出一轍,可以總結為一句話: 自頂向下拆分、自低向上合並。

    我們簡單回顧一下歸並排序的思路:

    1. 明確主函式的功能、輸入、輸出;

    2. 分解問題,確定分解後子函式的功能、輸入、輸出;

    3. 合並子函式的返回,虛擬碼如下:

    //主函式voidmergeSort(std::vector<int>& arr, int left, int right){int mid = left + (right - left) / 2;//拆分過程 一拆二 mergeSort(arr, left, mid); //子函式1 mergeSort(arr, mid + 1, right);//子函式2//合並過程 merge(arr, left, mid, right);}//合並函式voidmerge(std::vector<int>& arr, int left, int mid, int right){//實作}

    而領域驅動在實作的過程中依舊沿用了這個思路即: 定義問題、分解問題、合並結果。

    定義問題: 當我們面對一個復雜場景時,首先需要確定面對的問題是什麽?問題的邊界(限界上下文)在哪裏, 我們很容易理解解決問題帶來的價值,但是很容易忽略定義問題帶來的價值。 在計畫實踐中,不知道你有沒有遇到過這樣的一種場景:技術同學會根據產品同學的一段描述立馬會陷入到技術實作中,等到驗收的過程中才會說出:「哦,原來你只需要實作這種需求呀」的感嘆,這就是沒有找到核心問題所在。

    分解問題: 定義好問題即明確了問題的邊界,我們就可以在邊界內進行問題的劃分,領域驅動的核心思想是 分治 ,即拆分「邊界分明」的子問題,再針對子問題進行解決。這裏分解問題的思路,也和微服務拆分的思路有異曲同工之秒(說到微服務這裏丟擲兩個有趣的問題 ,第一:領域驅動是03年就提出來了的概念,為什麽一直到15年左右才漸漸被大家熟知;第二:微服務和領域驅動有什麽關系,我相信在讀完本文後,你心中自有答案)

    合並結果:解決完一個一個的子問題還不夠,如何把資訊串起來才是難點,在串起來的過程中就會出現耦合問題,這就又回到了一個軟體實踐中一個普遍的問題:如何做到 「高內聚、低耦合 ,其實細心的小夥伴已經發現了,這裏的分解問題的目標就對應著高內聚,而合並結果的目標則是低耦合。

    那些晦澀難懂的術語

    上一節我們講解了領域驅動所想解決的問題: 降低系統復雜度, 以及它解決問題的思路: 統一語言、模型抽象、分而治之。 明確了這兩點,我們再去看看其中的一些抽象的概念,相信你會有更深的理解。

    2.1 在邊界內做事:領域與子體

    概念上領域指:從事一種專門活動或者事業的範圍,這裏的重點在於範圍兩個字,範圍即邊界。不管我們去解決什麽問題,問題總是有邊界的,邊界越清晰,解決問題的思路則越清晰,再簡單點說, 領域就是在一個邊界內要解決的問題。 針對一個復雜問題,使用分治的思想,還可以進一步拆分成子問題。這種研究問題的思路其實已經司空見慣,假如我們需要研究人體,那麽人即問題的研究物件,我們可以按照不同的方法把把問題拆分成子問題,以下是兩個不同的思路,左圖是按照「系統 劃分,右圖是按照「組成 劃分。

    如何分解,沒有所謂的標準答案,拆分的方式不同,其實也可以說是抽象的角度的不同,因為抽象的角度不同,研究的方法也會有不同。比如中醫研究人體會側重於整體和部份的關系,西醫則側重於定量分析,我們不能說那種好或者不好,只是看待問題的角度不同,角度即抽象。

    當完成分解過程,我們在針對子問題,再尋求對應的解決思路, 這個過程就是從問題域到解決域的過程, 以下圖可以更直接的幫助你理解。

    2.2 領域按功能再劃分:核心域、通用域、支持域

    在不斷劃分的過程中,還可以按照功能性的不同把子領域再次的化為:核心域、通用域、支持域,這裏需要強調的是子體的劃分完全建立在對於業務的理解之上, 基於業務,而非技術。

  • 核心域

  • 是指富有競爭力的領域,這裏是仁者見仁、智者見智,不同的人對於競爭力有著不同的理解,比如還是拿人來舉例,身體、認知、財富到底哪一個是一個人的核心的競爭力,當認為是身體是核心的人就會側重於鍛煉健身;認為認知是核心的人則會側重於看書學習;認為財富重要的人則會側重於事業.... 總體看並不能說誰對誰錯,這是看待問題的方式不同。

    而對於公司也是一樣的道理,我們看很多公司的業務和產品,表面上很相似,但是其實有著完全不同的商業模式,就以電商平台舉例,有的核心領域在物流服務、高端的品質;有的核心領域則是更加便宜好用的貨物上,對於一個公司來說,劃定了一個核心領域,其實也就確定了資源投入的方向, 把好鋼用在刀刃上, 提供差異化的價值服務。

  • 通用域

  • 高復用能力或者沒有太多個人化需求的領域,比如所謂的「中台」概念就是指高復用的模組化服務,整合所有底層能力,快速叠代前台功能。

  • 支撐域

  • 支撐域是對核心域有所支持,但不是業務的核心競爭力的部份。這部份的業務規則相對簡單,通常不需要深入理解業務需求,只需要滿足基本的業務需求即可。

    2.3 實體和值物件

    什麽是實體?在業務中有 具有唯一標識的物件。 比如在電商場景下,一個物品物件就可以是一個實體,物品有唯一識別元(物品 id),物品的業務表現可能會發生變化,但是識別元在整個業務周期中是保持一致,比如一個物品在購買前是商品、購買後就變成了需發貨的貨物、如果要起退款就變成了一個需要召回的物品,但始終物品的識別元不會改變。

    什麽是值物件?針對一個實體物件,光有一個唯一標識是不夠的,它不足於描述物件的特性,所以就有了內容,比如一個商品的內容一般有名稱、價格、圖片、生產地,而 值物件就是一個業務實體內容的集合。

    在實踐中,業務實體往往對應著一個實體類,這個實體類有唯一的標識、內容、以及其所有的業務方法。領域驅動提倡使用充血模型的方式,即在類中實作所有相關的業務方法,而不是只把數據直接對外暴漏,這樣可以很好的保證了業務數據的一致性和封裝的特性,以下是一個物品物件的類實作。

    //實體 //物品類public class Product{privateString productId; //唯一主鍵 唯一標識 privateString productName; privateString productUrl; privateString productPrice; Private Address productAddress;// 內容集合// get set 業務行為 ...publicfunction(){}}//值物件 //倉庫地址類 (無主鍵id)public class ProductAddress{privateString Province;privateString City;privateString District;}

    2.4 聚合和聚合根

    當我們需要完成一個業務功能時,往往不是一個人就可以完成,而是大家協同工作,一起完成目標,在領域驅動中, 實體就好像我們每一個人,聚合就是可以讓我們協同工作的組織,聚合根就是這個組織的領導者, 所以聚合其實就是由業務邏輯緊密關聯的實體和值物件的集合,每個聚合有唯一的聚合根和業務邊界,聚合一般會根據業務單一職責和高內聚原則設計,來確定其需要包括哪些業務實體以及值物件。

    我們以購物車場景為例來體會一下聚合和聚合根的含義,在購物車中,加入購物車的商品列表構成了一個聚合,購物車 id 即聚合根,透過購物車 id,外界可以存取購買物品的列表資訊、狀態、下單總金額等。

    如果要用程式碼實作這個簡單的場景,我們很自然地想到可以把購物車的相關邏輯實作在一個微服務裏,實際上,在領域驅動中,一組相關的業務聚合往往透過一個微服務來實作。再初步了解領域驅動的相關概念後,我們梳理一下它們之間的關系,如圖所示。

    拆分與合並

    上一節我們已經講解了一些領域驅動中一些重要的概念,這一節我們會介紹領域驅動中,關於分治與合並思想的落地,下面我們就分別討論這兩個過程 。

    3.1 拆分與微服務架構

    我們還是回到歸並排序的案例中,思考一下平時所寫程式碼與歸並排序的相似之處,我們簡單的對歸並排序程式碼做一些改造,如下:

    voidmergeSort(std::vector<int>& arr, int left, int right){//拆分過程//把mergeSort(arr, left, mid)覆寫成 int resA=rpc.funtionA(); //把mergeSort(arr, mid + 1, right)覆寫成 int resB=rpc.funtionB(); //合並過程 merge(arr, left, mid, right)改為 func(resA,resB); }

    想想看,這不就是我們平常寫的套用層程式碼嗎,先遠端同步呼叫 A 服務獲取資訊,再同步呼叫 B 服務,再組合所有結果數據進行運算並返回,如下圖所示。

    這不就是微服務的分層架構嘛! 領域驅動最後的落地實作形式就是微服務, 到這裏就可以回答上面提出的一個問題,為什麽領域驅動的理念是02年提出了,但是到了15年後才被人熟知,就是因為雲原生、微服務架構的發展是在15年左右,這給領域驅動的理念提供了一片可以生存的土壤;相反領域驅動也給微服務的設計提供了必要的方法論。

    清楚微服務架構的同學一定知道:微服務架構設計的難點之一是拆分服務的力度大小,拆多了會導致運維難度呈指數提高,拆少了又回到了單體架構的模式、不夠靈活,中間這個度就需要一種方法論來指導,而這個方法論就可以是領域驅動。對比領域分析模型和微服務架構,你會發現其實都是相互對應的, 只是一種是從業務角度出發描述問題,一種是從技術實作角度描述問題, 而這也是理論和實踐的一種結合。

    3.2 合並的最佳實踐:領域事件

    在描述業務的過程中,往往會有這樣一種描述:當某種事件發生後,會觸發後續的事件或者使用者進一步的行為操作,在領域驅動中會把這種有明顯先後因果邏輯的事件稱之為領域事件。

    在領域事件中,會發現不同事件往往屬於不同的領域服務之間,比如使用者在購買物品支付成功後,會觸發發貨流程,這裏的支付和發貨就屬於不同的領域,並在邏輯上有先後的順序。

    針對以上事件,領域驅動提倡:領域事件的資料通訊方式使用事件釋出訂閱的方式進行,不直接同步呼叫, 而事件釋出的本質則是一種低耦合的異步數據溝通方式。

    同步呼叫有兩點需要考慮的問題:分布式事務以及時耗。針對事務問題一般需要引入第三方元件或者在業務層處理各種超時失敗等異常的場景,可以說相當的復雜,維護成本也很高;而在分布式系統中時耗問題會被放大,一個請求可能跨越十幾個甚至幾十個服務,高並行的場景下,超時的風險會增大,上遊的介面可能會被拖死。這都是同步呼叫需要考慮的問題。

    在領域事件這種場景下,有一個更好技術選擇,則是使用事件釋出訂閱的方式,還是拿使用者購買物品支付發貨場景為例,看看其實作過程:

    1. 使用者支付下單後,支付域建立事件,持久化事件狀態,在支付成功後釋出事件,支付行為結束。

    2. 發貨域訂閱支付事件,在收到使用者支付成功事件後,觸發使用者所購買物品的發貨,持久化事件狀態並結束。

    3. 使用者收到發貨成功通知,等待收貨。

    領域事件的本質其實是透過分析使用者旅程找到領域之間的因果邏輯鏈,再透過事件釋出訂閱機制去實作流程上的解耦合。

    那我們如何找到領域事件呢?一種最佳的實踐是在領域專家的主導下計畫相關的同學一起進行頭腦風暴,聯想和關聯到和業務有關的所有事件, 但是這裏的難點並不是如何發散,而是發散後如何收斂事件, 收斂的本質是對於事件的有效分類,這需要可以洞悉業務本質的人才可以做到,所以這就是為什麽領域驅動中有一種角色叫領域專家 ,這個過程我也用圖來表示。


    總結

    以上就是我對於領域驅動的一些淺見,如果你看完後還是感覺領域驅動有點形而上學,沒關系,只要你記住, 不管是技術還是生活,遇到事情多溝通,復雜問題先分解。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:呂昊俁

    來源:騰訊雲開發者

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]