當今微服務盛行之架構師必經之路-領域驅動設計-上

DDD基礎

引言

<<領域驅動設計-軟件核心複雜性應對之道>>:全書圍繞着設計和開發實踐,結合若干真實的項目案例,向讀者闡述如何在真實的軟件開發中應用領域驅動設計。書中給出了領域驅動設計的系統化方法,並將人們普遍接受的一些最佳實踐綜合到一起,融入了作者的見解和經驗,展現了一些可擴展的設計最佳實踐、已驗證過的技術以及便於應對複雜領域的軟件項目開發的基本原則。適合各層次的面向對象軟件開發人員、系統分析員閱讀。

<<代碼精進之路從碼農到工匠>>:共有13章內容,主要分為技藝部分、思想部分和實踐部分。技藝部分詳細介紹了編程技巧和方法論,並配以詳盡的代碼案例,有助於讀者提高編寫代碼的能力,優化代碼質量。思想部分主要包括抽象能力、分治思想,以及程序員應該具備的素養等內容。實踐部分主要介紹了常見的應用架構模式,以及COLA架構的設計原理。

DDD的革命性在於領域驅動設計是面向對象分析的方法論,它可以利用面向對象的特性(封裝、多態)有效地化解複雜性,而傳統J2EE或Spring+Hibernate等事務性編程模型只關心數據。這些數據對象除了簡單的setter/getter方法外,不包含任何業務邏輯,業務邏輯都是以過程式的代碼寫在Service中。這種方式極易上手,但隨着業務的發展,系統也很容易變得混亂複雜。

學習、理解及應用領域驅動設計建議先有以下幾個基礎條件:

  • 有編程語言基礎
  • 面向對象設計
  • 了解設計模式
  • 有項目經驗和架構設計經驗

隨着軟件系統越來越龐大,需求越來越模糊,代碼越來越混亂,測試越來越困難,技術演進基本不可能,而其中大型複雜的軟件項目更容易走向系統老化的過程,形成需求難、開發難、測試難、創新難,單體架構局部業務膨脹可以拆成微服務,那麼微服務局部業務膨脹又應該怎麼做?DDD之所以火,即能解決微服務解決不了的問題。DDD是為了解決快速變化、複雜系統的設計問題。

image-20220304170616273

如何解決系統老化問題使得重新崛起的DDD領域驅動設計成了業界最大的希望乃至目前階段最理想的方式,積極踐行DDD,搭建的每一個應用,實現的每一個功能,寫的每一行代碼,都是在精修架構思維的內功。過去系統分析和系統設計都是分離的,DDD則打破了這種隔閡,提出了領域模型概念,統一了分析和設計編程,使得軟件能夠更靈活快速跟隨需求變化。先來理解幾個概念:

  • POP:Procedure Oriented Programming即面向過程編程,是以功能為中心來進行思考和組織的一種編程方法,它強調的是功能(即系統的數據被加工和處理的過程),在程序設計中主要以函數或者過程為程序的基本組織方式,系統功能是由一組相關的過程和函數序列構成。從思維上來講,面向過程更強調細節,忽視了整體性和邊界性。
  • OOP:Object Oriented Programming即面向對象編程,是以對象為中心,面向對象作為一種新型的程序設計方法,其是以對象模型為基礎進行的抽象過程,並在應用過程中形成了描述自己的抽象概念定義。面向對象是一種編程範式,滿足面向對象編程的語言,一般會提供類、封裝、繼承等語法和概念來輔助我們進行面向對象編程,也即是以對象作為邊界。
  • AOP:Aspect Oriented Programming即面向切面編程,是以通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

思想

傳統架構的設計邏輯從數據庫出發,先進行數據庫設計、建表、字段不合理再調整,表和對象進行管線。而DDD是把數據庫當成與其他組件相等的一環,可以解決現有系統因為數據庫,而導致系統耦合問題。而適應快節奏的需求變更頻繁的敏捷開發的出現,更是對傳統架構的設計產生較大影響,架構師作為架構設計文檔和圖形的載體。

DDD中的設計主要指領域模型的設計。為什麼是領域模型的設計而不是架構設計或其他的什麼設計呢?因為DDD是一種基於模型驅動開發的軟件開發思想,強調領域模型是整個系統的核心,領域模型也是整個系統的核心價值所在。每一個領域,都有一個對應的領域模型,領域模型能夠很好的幫我們解決複雜的業務問題。領域模型設計只是整個軟件設計中的很小一部分。除了領域模型設計之外,要落地一個系統,我們還有非常多的其他設計要做,比如容量規劃、架構設計、數據庫設計、緩存設計、框架選型、發佈方案、數據遷移、同步方案、分庫分表方案、回滾方案、高並發解決方案、一致性選型、性能壓測方案、監控報警方案等。

一般系統設計分為概念設計、邏輯設計、物理設計三個階段。

  • 概念設計:涵蓋下下1-3步
    • 輸出概念關聯圖、概念類、領域類圖、填充屬性的屬性完備概念類
    • 主要人員:架構師、產品、運營方
  • 邏輯設計:
    • 輸出業務邏輯、業務模型、對象模型、業務用例場景、業務用例規約、可復用的業務邏輯common
    • 主要人員:架構師、技術經理、技術專家、核心開發人員。
  • 物理設計:
    • 輸出存儲設計如索引、分區、分庫分表
    • 主要人員:設計和開發人員

image-20220302162538332

定義

  • DDD(Domain-Driven Design)領域驅動設計,擴大上述概念的邊界(其問題域為邊界),將對象組裝成領域。
  • DDD本質是一種軟件設計思想和程序分析設計方法,一般為需求分析人員使用,不關乎具體的技術,其具體代碼實現依然是以OOP和AOP為主。而架構風格又有很多,比如常見架構風格有微服務架構、SOA(面向服務的)架構、REST風格架構、CQRS架構、事件驅動架構。
  • DDD是一套綜合軟件系統分析和設計的面向對象建模方法,領域驅動設計作為針對大型複雜業務系統的領域建模方法體系(不僅限於面向對象的領 域建模),它改變了傳統軟件開發工程師針對數據庫建模的方式,通過面向領域的思維方式,將要解決的業務概念和業務規則等內容提煉為領域知識,然後藉由不同的建模範式將這些領域知識抽象為能夠反映真實世界的領域模型。
  • 領域驅動設計和微服務的關係:微服務架構怎麼拆、拆多小?領域驅動設計定義領域模型,從而劃分領域邊界,然後再根據我們的領域邊界從業務的角度去進行微服務邊界定義。
  • 領域驅動用什麼方式進行邊界的定義?
    • 戰略設計:從業務角度,建立業務模型,劃分業務的邊界。
    • 戰術設計:根據業務模型進行技術實現,完成軟件的開發和落地。

組成

領域

基本定義

一個領域本質上可以理解為一個問題域,只要是同一個領域,那問題域就相同。任何一個系統都會屬於某個特定的領域,比如論壇是一個領域,只要你想做一個論壇,那這個論壇的核心業務是確定的,比如都有用戶發帖、回帖等核心基本功能;傳統電商領域的問題無非就是訂單、商品、支付、物流、庫存之類,而社交電商除了傳統電商的屬性外,也會附加社交、通訊相關的功能。所以同一個領域的系統都具有相同的核心業務,因為他們要解決的問題的本質是類似的

只要我們確定了系統所屬的領域,那這個系統的核心業務,即要解決的關鍵問題、問題的範圍邊界就基本確定了。通常我們說,要成為一個領域的專家,必須要在這個領域深入研究很多年才行。因為只有你研究了很多年,你才會遇到非常多的該領域的問題,同時你解決這個領域中的問題的經驗也非常豐富。很多時候,領域專家比技術專家更加吃香,比如金融領域的專家。

領域劃分以事件風暴的形式(Event Storming),列出所有的用戶故事(Use Story),用戶故事可通過6W模型來構建,即描寫場景的 Who、What、Why、Where、When 與 hoW 六個要素。然後圈選功能相近的部分,就形成了領域,領域又根據職能不同劃分為:核心域、支撐域、通用域,

image-20220306160059559

驅動

領域驅動領域模型設計,領域模型驅動代碼實現。這個就和我們傳統的數據庫驅動開發的思路形成對比了。DDD中,我們總是以領域為邊界,分析領域中的核心問題(核心關注點),然後設計對應的領域模型,再通過領域模型驅動代碼實現。而像數據庫設計、持久化技術等這些都不是DDD的核心,而是外圍的東西。

領域驅動設計(DDD)的最大價值:當我們要開發一個系統時,應該盡量先把領域模型想清楚,然後再開始動手編碼,這樣的系統後期才會很好維護。但是,很多項目(尤其是互聯網項目,為了趕工)都是一開始模型沒想清楚,一上來就開始建表寫代碼,代碼寫的非常冗餘,完全是過程是的思考方式,最後導致系統非常難以維護。而且更糟糕的是,出來混總是要還的,前期的領域模型設計的不好,不夠抽象,如果你的系統會長期需要維護和適應業務變化,那後面你一定會遇到各種問題維護上的困難,比如數據結構設計不合理,代碼到處冗餘,改BUG到處引入新的BUG,新人對這種代碼上手困難,等。而那時如果你再想重構模型,那要付出的代價會比一開始重新開發還要大,因為你還要考慮兼容歷史的數據,數據遷移,如何平滑發佈等各種頭疼的問題。所以,就導致我們最後天天加班。從面向過程式的想到哪裡寫到哪裡的思想轉變為基於系統化的模型驅動的思維很難,這或許是DDD很難在中國或國外流行起來的原因吧。

設計

DDD中的設計主要指領域模型的設計DDD是一種基於模型驅動開發的軟件開發思想,強調領域模型是整個系統的核心,領域模型也是整個系統的核心價值所在。每一個領域,都有一個對應的領域模型,領域模型能夠很好的幫我們解決複雜的業務問題,從領域和代碼實現的角度來理解,領域模型綁定了領域和代碼實現,確保了最終的代碼實現就一定是解決了領域中的核心問題的。

子域

子域可以理解為更加細分的領域,甚至可以把子域進行更新劃分,分成更多的子域。比如把電商平台看成是一個領域,那麼訂單、倉儲、物流都可以是子域,而倉儲可以劃分為本地倉儲、三方倉儲、異地倉儲等子域。子域可以劃分為三種類型

  • 核心子域:核心域是整個業務系統的核心,所有業務都要圍繞着核心業務域展開。精鍊業務域,包括領域願景說明、突出核心、內聚機制、分離的核心、抽象核心。比如淘寶來說,淘寶屬於CToC,核心是交易保證,也即是保障賣家能把貨賣出去、買家能獲取貨品;京東BToC,核心保障口碑,注重產品質量,包括採購、倉儲、物流、供應鏈等環境的質量。
  • 通用子域:整個領域都能用到子域
  • 支撐子域:不包含核心競爭力的功能,也不包含通用的功能,但又是必須支撐的。

通用語言

能夠正確的、簡單的、清晰的表達業務,並讓項目的參與人員都能夠達成共識的語言。統一語言是提煉領域知識的輸出結果,也是進行後續需求迭代及重構的基礎,統一語言的建立有以下幾個要點:

  • 統一語言必須以文檔的形式提供出來,並且在整個項目組的各團隊達成共識;
  • 統一語言必須每個中文名有對應的英文名,並且在整個技術棧保持一致;
  • 統一語言必須是完整的,包含以下要素:
    • 領域模型的概念與邏輯;
    • 界限上下文(Bounded Context);
    • 系統隱喻;
    • 職責的分層;
    • 模式(patterns)與慣用法。

限界上下文

語義問題,蘋果不大好吃這句話可以理解為蘋果,不大好吃,也可以理解為蘋果不大,好吃。限界上下文用來封裝封裝通用語言和領域對象,提供上線文環境,保證在領域內的一些術語、業務相關對象等有一個確切的含義、沒有二義性,這個邊界定義模型的使用範圍。那上面例子來說,蘋果不大好吃,下次再也不買了。

限界上下文包含兩部分:上下文(Context)是業務目標,限界(Bounded)則是保護和隔離上下文的邊界。限界上下文沒有統一的劃分標準,需根據自己的業務場景來甄別如何劃分。一個上下文中包含了相同的領域知識,角色在上下文中完成動作目標;邊界體現在以下幾方面:

  • 領域邏輯層:確定了領域模型的業務邊界,維護了模型的完整性與一致性,從而降低系統的業務複雜度;
  • 團隊合作層:限界上下文一般也是用戶換分團隊的依據;
  • 技術實現層:限界上下文可當成是微服務的劃分邊界;

戰略設計和戰術設計

戰略設計是一種用高層次的視野來審視軟件系統的方式,戰略設計也可以出現建模的方式問題、頻繁進行戰略改動問題。UML建模,適合小範圍。關聯關係、引用關係,解決複雜問題如四色建模法、限界紙筆法、事件風暴。

  • 問題空間:對問題空間進行合理分解,識別出核心子領域、通用子領域和支撐子領域, 並確定各個子領域的目標、邊界和建模策略。
  • 解空間:對問題空間進行解決方案的架構映射,通過劃分限界上下文,為統一語言提供知 識語境,並在其邊界內維護領域模型的統一。每個限界上下文的內部有着自己的架構, 限界上下文之間的協作關係則通過上下文映射來體現和表達。
  • 戰術設計階段需要在限界上下文內部開展領域建模,前提是你為限界上下文選擇了領域模型 模式。在限界上下文內部,需要通過分層架構將領域獨立出來,在排除技術實現的干擾下,通過與 領域專家的協作在統一語言的指導下逐步獲得領域模型。
  • 戰術設計階段最重要的設計元模型是聚合模式。雖然聚合是實體和值對象的概念邊界, 然而 在獲得了清晰表達領域知識的領域模型後,我們可以將聚合視為表達領域邏輯的最小設計單元。如 果領域行為是無狀態的,或者需要多個聚合的協作,又或者需要訪問外部資源,則應該將它分配給 領域服務。至於領域事件,則主要用於表達領域對象狀態的遷移,也可以通過事件來實現聚合乃至 限界上下文之間的狀態通知。
  • 從戰略設計到戰術設計是一個自頂向下的設計過程,體現為設計原則對設計決策的指導;將 戰術設計方案反饋給戰略設計,則是自底向上的演化過程,體現為對領域概念的重構引起對戰略架 構的重構。二者形成不斷演化、螺旋上升的設計循環。

領域模型

領域模型是對領域內的概念類或現實世界中對象的可視化表示。包括業務對象模型、業務對象之間的引用關係。

  • 業務對象包含一下三種

    • 業務角色:表示一個角色以及它所承擔的一系列職責,比如收銀員,職責計算商品價格、收錢、找零、退換貨等。
    • 業務實體:表示的其實就是你與業務角色交互所需要的可交付的工件、資源、事件,比如電商里的商品、發票。
    • 業務用例:表示的就是我們業務角色和業務實體之間是如何執行工作流程,比如業務鏈路、測試用例。
  • 領域建模方法

    • 劃分好邊界上下文,通常每個子域(sub domain)對應一個邊界上下文(bounded context),同一個邊界上下文中的概念是明確的,沒有任何歧義;
    • 在每個邊界上下文中設計領域模型,具體的領域模型設計方法有很多種,如以場景為出發點的四色原型分析法,這個步驟最核心的就是找出聚合根,並找出每個聚合根包含的信息,前提是先能設計聚合。
    • 畫出領域模型圖,圈出每個模型中的聚合邊界;
    • 設計領域模型時,要考慮該領域模型是否滿足業務規則,同時還要綜合考慮技術實現等問題,比如並發問題;領域模型不是概念模型,概念模型不關注技術實現,領域模型關心;所以領域模型才能直接指導編碼實現;
    • 思考領域模型是如何在業務場景中發揮作用的,以及是如何參與到業務流程的每個環節的;
    • 場景走查,確認領域模型是否能滿足領域中的業務場景和業務流程;
    • 模型持續重構、完善、精鍊;
  • 領域模型的核心作用:

    • 抽象了領域內的核心概念,並建立概念之間的關係;
    • 領域模型承擔了領域內的狀態的維護;
    • 領域模型維護了領域內的數據之間的業務規則,數據一致性;

業務對象模型

業務對象模型就是業務邏輯流轉的過程中需要的所有角色,甚至還包括你的業務邏輯流轉本身。業務模型在DDD大概實現有四種方式

  • 失血模型:對象中只會包含get和set方法,業務邏輯都散落在Service層裏面。
    • 優點
      • 領域對象結構簡單
    • 缺點
      • 腫脹的業務代碼邏輯,難以維護。
      • 無法應對頻繁更改的需求。

image-20220306095126190

  • 貧血模型:可以分為固有行為和非固有行為,比如人的類,固有行為有吃飯、睡覺,非固有行為如編程、打遊戲。

    • 優點
      • 層級結構清楚,更層級是單向依賴。
      • 對於只有少量業務邏輯的應用來說,使用起來非常自然。
      • 開發非常迅速,易於理解。
    • 缺點
      • 無法良好應對複雜的邏輯以及場景。

    image-20220306100353575

  • 充血模型

    • 優點
      • 更加符合面向對象的原則。
      • 業務邏輯層很薄,符合單一職責原則。
    • 缺點
      • 職責不好劃分,開發者水平要很高。
      • 模型中包含了大量的操作,當去實例化的時候會增加很多不必要的消耗。

image-20220306101246030

  • 漲血模型:直接就取消掉Service層
    • 優點
      • 簡化分層架構。
      • 也算是符合面向對象設計原則。
    • 缺點
      • 取消業務邏輯層,直接在Domain Object上封裝事務以及授權,因此授權很多原本不屬於這個領域對象的邏輯。當模型不穩定時會影響到代碼穩定性和維護的穩定性。

image-20220306101553965

失血模型和漲血模型是現在不太推薦的使用的模型,貧血模型是用的最多的,而充血模型對於程序員的要求非常高。貧血模型的對象會被各種Service調用,分佈在不同業務中,對於業務理解難度加大。而充血模型每個實體操作自己實體的變化,跨實體的變化通過領域服務實現,領域服務調用實體方法完成狀態改變。

**本人博客網站 **IT小神 www.itxiaoshen.com