主頁 > 軟體設計 > 萬字長文助你上手軟體領域驅動設計 DDD

萬字長文助你上手軟體領域驅動設計 DDD

2023-01-20 07:23:50 軟體設計

最近看了一本書《解構-領域驅動設計》,書中提出了領域驅動設計統一程序(DDDRUP),它指明了實踐 DDD 的具體步驟,并很好地串聯了各種概念、模式和思想,因此,我對書本內容做了梳理、簡化,融入自己的理解,并結合之前閱讀的書籍以及實踐經驗,最終形成這篇文章,希望可以幫助大伙理順 DDD 的各種概念、模式和思想,降低上手 DDD 的門檻,

1.背景

領域驅動設計(DDD)由 Eric Evans 提出,并一經《領域驅動設計:軟體核心復雜性應對之道》的發布,在軟體行業中引起了不少的轟動,DDD 提供的一種新穎的,甚至有點“另類”的思維方式,它在告訴軟體開發者“我們要用業務方案來解決業務問題,而不是技術方案解決業務問題”,有點魔法打敗魔法的意思,DDD 雖然讓人眼前一亮,但是所提倡的理念有點“違背直覺”(對開發人員而言),因此,在當時并沒有流行開來,

后來,微服務架構的興起,大伙驚奇地發現 DDD 是作為劃分“微服務邊界”的一把利器,并且 DDD 提及的很多設計理念與微服務架構十分契合,因此 DDD 逐漸被開發者們接受并流行起來,毫不夸張地說,了解和學習 DDD 可以算得上是如今軟體行業從業者的一門必修課了,

但是!DDD 的學習曲線較為陡峭,作為一個小白,翻閱過很多相關的書籍、KM 文章和分享,但始終覺得未得要領、一知半解,原因有二:a) DDD 涉及的概念繁多,且不同概念的抽象層次不一樣,如果我們直白地去理解,往往會感到疑惑,比如:子域和限界背景關系都是用于將問題進行歸類和收斂,他們的區別是什么?b)缺少程序指導,難以將概念有序的串聯起來,作為方法論,DDD 給出了設計思想,核心原則以及常用工具,但是卻缺少細致有序的方法步驟,導致難以上手實踐,

幸運的是,最近看了一本書《解構-領域驅動設計》,這本書提出了領域驅動設計統一程序(DDDRUP),它指明了實踐 DDD 的具體步驟,并很好地串聯了各種概念、模式和思想,因此,我對書本內容做了梳理、簡化,融入自己的理解,并結合之前閱讀的書籍以及實踐經驗,最終形成這篇文章,希望可以幫助大伙理順 DDD 的各種概念、模式和思想,降低上手 DDD 的門檻,

 

2.DDD 概要與實踐感悟

經典必讀書籍《領域驅動設計:軟體核心復雜性應對之道》的書名包含了兩個關鍵詞:領域驅動復雜性,分別代表了 DDD 的核心原則以及解決的問題,

2.1 復雜性

系統的復雜性往往并不在技術上,而是來自領域本身、用戶的活動或業務服務,當這種領域復雜性在設計中沒有得到解決時,基礎技術的構思再好也是無濟于事,而系統的復雜度體現在三個方面:規模結構變化

規模:指的是系統所支持的功能點,以及功能點與功能點之間的的關系,DDD 通過子領域,限界背景關系,聚合等模式對問題進行拆分和歸類,不斷收窄問題域,保證聚合邊界內所解決的問題集合足夠收斂和可控,

結構:指的是系統架構,系統架構是否分層;若分層,每層劃分的職責邊界是否清晰;架構的基本管理單元是什么,它決定了架構演進時的復雜度,DDD 通過分層架構,獨立出領域層,且架構中的每層都有清晰的職責,整體架構的基本管理單元是聚合,它是一個完整的、自治的管理單元,當需要進行服務拆分時,可以直接以聚合作為基本單元進行拆分,

變化:指的是系統回應需求變化的能力,快速回應變化的有效手段是分離不易變邏輯和易變邏輯,"以不變應萬變",而通過分層架構獨立的領域層正是不易變的邏輯,領域層是對領域知識的封裝,其提供的領域服務具有經驗性和前瞻性,是對領域內穩定的領域規則的表達,而領域層以外的應用層和基礎設施層則是易變邏輯的封裝,保證核心的獨立和穩定,通過在調整應用層和基礎設施層來實作快速回應需求變化,

2.2 領域驅動

領域驅動指的是以領域作為解決問題切入點,面對業務需求,先提煉出領域概念,并構建領域模型來表達業務問題,而構建程序中我們應該盡可能避免牽扯技術方案或技術細節,而編碼實作更像是對領域模型的代碼翻譯,代碼(變數名、方法名、類名等)中要求能夠表達領域概念,讓人見碼明義,

結合實踐經驗,以下是本人對“領域驅動”的一些見解:

思維模式轉變

實踐 DDD 以前,我最常使用的是資料驅動設計,它的核心思路針對業務需求進行資料建模:根據業務需求提煉出類,然后通過 ORM 把類映射為表結構,并根據讀寫性能要求使用范式優化表與表之間的關聯關系,資料驅動是從技術的維度解決業務問題,得出的資料模型是對業務需求的直接翻譯,并沒有蘊含穩定的領域知識/規則,一旦需求發生變化,資料模型就得發生變化,對應的庫表的設計也需要進行調整,這種設計思維導致變化從需求穿透到了資料層,中間并沒有穩定的,不易變的層級進行阻隔,最終導致系統回應變化的能力很差,

協同方式轉變

過去由產品同學提出業務需求,研發同學根據業務需求的 tapd 進行技術方案設計,并編程實作,

這種協同方式的弊端在于:無法形成能夠消除認知差異的模型,產品同學從業務角度提出用戶需求,這些需求可能是易變的、定制化的,而研發同學在缺少行業經驗的情況下,往往會選擇直譯,即根據需求直接轉換為資料模型,而研發同學從技術實作角度設計技術方案,其中涉及很多的技術細節,產品同學無法從中判斷是否與自己提出的業務訴求和產品規劃相一致,最終形成認知差異,且認知差異會隨著迭代不斷被放大,最后系統變成一個大泥球,

圖片

DDD 通過解鎖新角色”領域專家"以及模型驅動設計,有效地降低產品和研發的認知差異,領域專家是具有豐富行業經驗和領域知識儲備的人,他們能夠在易變的、定制化的需求中提煉出清晰的邊界,穩定的、可復用的領域概念和業務規則,并攜手產品和研發共同構建出領域模型,領域模型是對業務需求的知識表達形式,它不涉及具體的技術細節(但能夠指導研發同學進行編程實作),因此消除了產品和研發在需求認知上的鴻溝,而模型驅動設計則要求領域模型能夠關聯業務需求和編碼實作,模型的變更意味著需求變更和代碼變更,協作圍繞模型為中心,

圖片

精煉回圈

精煉回圈指的是在統一語言,提煉領域概念,明確邊界,構建模型,系結實作程序中,這些環節相互影響和反饋,在不斷的迭代試錯-調整以最終沉淀出穩定的、深層次的模型的程序,比如,我們在提煉領域概念的時候會覺得統一語言定義不合理/有歧義,此時我們就會調整統一語言的定義,并重新進行提煉領域概念,通過精煉回圈,我們逐步形成穩定的領域模型,在 DDD 中,讓領域專家來主導概念提煉、邊界劃分等宏觀設計,原因就在于領域專家的經驗和行業洞見來源于過去已經迭代的無數個精煉回圈,因此由這些宏觀設計推匯出來的領域模型,往往都是非常穩定的,

精煉回圈的核心是回圈,它避免知識只朝單一方向流動,最終因各環節上的認知差異,最終導致模型無法在產品、領域專家和研發中達成一致、模型與實作割裂,

2.3 怎么才算 DDD?

我早期實踐 DDD 的時候,認為代碼分層遵循四層架構就是 DDD,抑或分離介面和實作,實作下沉至基礎設施層就是 DDD,實則不然,結合上述內容,目前個人認為只要滿足以下條件即為實踐 DDD:

  • 構建出產品、領域專家和研發同學認知一致且便于交流的模型,并且模型與實作緊密系結;
  • 模型逐步演進,反復消化和精煉;
  • 模型蘊含領域知識,足夠穩定,

3.問題空間&解空間

3.1 問題空間&解空間

問題空間和解空間并非 DDD 特有的概念,而是人們為了區分真實世界和理念世界而提出的概念,問題空間表示的是真實世界,是具體的問題、用戶的訴求,而解空間則是針對問題空間求解后構建的理念世界,其中包括了解決方案、模型等,

DDD 提出的戰略設計覆寫了問題空間和解空間,而戰術設計則聚焦在解空間上,明確 DDD 中的概念是作用于問題空間還是解空間,更有助于我們理解它們,

3.2 示例-學生管理系統的問題空間

學生管理系統(Student Management System,下文簡稱 SMS)作為 DDDRUP 的講解示例,以下為其問題空間的描述,

學校需要構建一個學生管理系統(Student Management System, SMS),

通過這個管理系統,學生可以進行選課,查詢成績,查詢績點,

而老師則可以通過這個系統錄入授課課程的成績,錄入的分數會由系統自動換算為績點,規則如下:若分數>= 90,績點為4.0;90>= 分數> 80,績點為3.0;80 >= 分數 > 70,績點為2.0;70 >= 分數 >= 60,績點為1.0;成績< 60,則沒有績點,并郵件通知教務員,由教務員聯系學生商榷重修事宜,

成績錄入后的一周內,若出現錄入成績錯誤的情況,老師可提交修改申請,由教務員審核后即可完成修改,審核完成后系統會通過郵件告知老師審核結果,一周后成績將鎖定,不予修改,成績鎖定后,次日系統會自動計算各年級、各班的學生的總績點(總績點由各門課程的學分與其績點進行加權平均后所得),

而教務員則可以通過該系統發布可以選修的課程,同時,教務員能夠查看到各年級,各班的學生的總績點排名,

4.領域驅動設計統一程序(DDDRUP)

雖然領域驅動設計劃分了戰略設計戰術設計,也提供了諸多模式和工具,但卻沒有一個統一程序去規范這兩個階段需要執行的活動、交付的工件以及階段里程碑,甚至沒有清晰定義這兩個階段如何銜接、它們之間執行的作業流到底是怎么樣的,

而《解構-領域驅動設計》提出的 DDDRUP 給出了更細致的步驟、步驟與步驟之間的銜接,以及明確的階段里程碑,最重要的是 DDDRUP 可以串聯 DDD 的所有概念和模式,非常便于初學者做知識梳理和上手實踐,下文我會依照 DDDRUP 的步驟流程進行講述,而非戰略設計+戰術設計的思路,(DDDRUP 各步驟與戰略&戰術設計的關系見下表),

圖片

 

5.全域分析階段

全域分析階段對問題空間進行的梳理和分析,形成統一語言(ubiquitous language), 獲取問題空間的價值需求以及業務需求

5.1 形成統一語言

統一語言:蘊含領域知識的、團隊內統一的領域術語,產品、領域專家以及開發人員掌握的領域知識存在差異,往往導致對同一個事物使用不同的術語,比如,商品的價格(Price)和商品的金額(Amount),它們本質是同一個東西,但是卻有不同的術語表示,

統一語言會參與 DDDRUP 的全流程,且會在精煉回圈程序中不斷進行調整,以反映出更合適、更深層次的領域知識,

根據業務需求形成統一語言,有助于團隊對事物的認知達成一致,統一語言可以通過詞匯表的形式展示,其中詞匯表最好還要包含術語對應的英文描述,便于研發同學在代碼層面表達統一語言,示例-SMS 的統一語言詞匯表如下,

圖片

5.2 價值需求分析

價值需求分析主要做的三個作業是:

  1. 識別利益相關者,利益相關者指的是與目標系統存在利益關系的人、團隊或組織, 可以簡單理解為目標系統的用戶,或與目標系統有直接互動的人、團隊或組織,
  2. 明確系統愿景,闡明目標系統要做什么,以及為何要做,
  3. 確定系統范圍,確定系統問題空間的邊界,明確系統什么該做,什么不該做,結合目標系統當前狀態未來狀態進行判斷,當前狀態指的是系統的可用資源,包括業務資源、人力資源,資金資源等;而未來的狀態則由業務目標、組織的戰略規劃和產品規劃共同構成,

并非任何系統都 DDD,DDD 的核心是解決領域復雜性,若系統邏輯簡單,功能不多,引入 DDD 則會得不償失,而在進行價值需求分析后,我們便能判斷是否需要通過 DDD 驅動系統的設計,

5.3 業務需求分析

5.3.1 業務流程、業務場景、業務服務和業務規則

使用業務流程、業務場景、業務服務業務規則來表示業務需求,

業務流程:表示的是一個完整的、端對端的服務程序,

業務場景:按階段性的業務目標劃分業務流程,就可以獲得業務場景,在示例-SMS 中,老師修改成績就分為了老師“提交申請單”,以及教務員“同意申請單”兩個場景,

業務服務:角色主動向目標系統發起服務請求完成一次完整的功能互動,以實作業務目標,角色可以用戶、策略(定時任務)或者其他系統,完整則強調的是業務服務的執行序列的所有步驟都應該是連續且不可中斷的,業務服務是業務需求分析最核心,也是最基礎的單元,而業務流程和業務場景是為了更好地分析出業務服務,在示例-SMS 中的“同意申請單”場景中包含了兩個業務服務:教務員“同意申請單”和系統“郵件通知”教務員,

業務規則:指對業務服務約束的描述,用于控制業務服務的對外行為,業務規則是業務服務正確性的基礎,常見的業務規則有:a) 意如“若… , 就….” 的需求描述,比如示例-SMS 中可提煉出“若成績錄入時間間隔超過一周,不予修改”;b) 具有事務性的操作,

圖片
5.3.2 子領域

通過業務流程、業務場景和業務服務的梳理,基本可以分析出業務需求所需要的業務服務,然而,業務服務粒度太細,而問題空間又太大,我們需要找一個更粗粒度的業務單元,來幫助我們對業務服務進行聚類,一方面可以降低管理過多細粒度業務服務導致的額外復雜度,另一方面可以幫助領域專家和開發團隊分析問題和設計方案時不至于陷入到業務細節中,而這個更粗粒度的業務單元就是子領域

子領域的作用

  • 劃分問題空間,作為業務服務分類的邊界;
  • 用于分辨問題空間的核心問題和次要問題,

子領域的分類:

  • 核心子領域:能夠體現系統愿景,具有產品差異化和核心競爭力的業務服務;
  • 通用子領域:包含的內容缺乏領域個性,具有較強的通用性,例如權限管理和郵件管理;
  • 支撐子領域:包含的內容多為“定制開發”,其為核心子領域的功能提供了支撐,

子領域的功能分類策略:問題空間應該分為哪些子領域,需要團隊對目標系統整體進行探索,并根據功能分類策略進行分解,

  • 業務職能:當目標系統運用于企業的生產和管理時,與目標系統業務有關的職能部門往往會影響目標系統的子領域劃分,并形成一種簡單的映射關系,這是康威定律的一種運用,
  • 業務產品:當目標系統為客戶提供諸多具有業務價值的產品時,可以按照產品的內容與方向進行子領域劃分,
  • 業務環節對貫穿目標系統的核心業務流程進行階段劃分,然后按斬訓分出來的每個環節確定子領域,(這也是我們最常用的策略)
  • 業務概念:捕捉目標系統中一目了然的業務概念,將其作為子領域,

劃分子領域的程序存在很多經驗因素,一個對該行業領域知識了如指掌的領域專家,可以在完成價值需求分析后,結合自身的領域經驗,能夠選擇合適的聚類策略并給出穩定的子領域串列,但,沒有領域經驗也沒有關系!因為根據知識消化回圈思路,再經歷多個迭代后收斂出來的子領域劃分也會逐漸合理,逼急領域專家憑經驗得出的子領域劃分,只是可能需要的時間要長一些,

 

6.架構映射階段

在架構映射階段,我們需要識別限界背景關系,并通過背景關系映射表示限界背景關系之間的協作關系,

6.1 限界背景關系的定義和特征

6.1.1 限界背景關系的定義

限界背景關系是語意和語境的邊界,在問題空間,統一語言形成了團隊對領域概念的統一表達,子領域形成了領域概念之間的邊界,而在解空間,限界背景關系可以看做是統一語言+子領域的融合體,統一語言需要在限界背景關系內才具有明確的業務含義,

以電商購物場景為例,在進行商品下單后,系統會生成一個訂單;在用戶付款完成后,系統也會生成一個訂單;到了物流派送流程,系統還會生成一個訂單,雖然這三個步驟中的領域概念都叫訂單,但是他們的關注點/職責卻不同:商品訂單關注的是商品詳情,支付訂單關注的是支付金額和分潤情況,物流訂單關注的是識訓地址,也就是說,商品、支付和物流分別為三個限界背景關系,而訂單作為統一語言需要在特定的限界背景關系內,我們才能夠明確其關注點/負責的職責,

6.1.2 限界背景關系的特征

最小完備:限界背景關系在履行屬于自己的業務能力時,擁有的領域知識是完整的,無須針對自己的資訊去求助別的限界背景關系,

自我履行:限界背景關系能夠根據自己擁有的知識來完成業務能力,自我履行體現了限界背景關系縱向切分業務能力的特征,

這里需要強調一下業務模塊(橫向切分)限界背景關系(縱向切分)的區別,業務模塊不具備完整、獨立的業務能力,它沒有按照同一個業務變化的方向進行,而限界背景關系是對目標系統架構的縱向切分,切分的依據是從業務進行考慮的領域維度,為了提供完整的業務能力,在根據領域維度進行劃分時,還需要考慮支撐業務能力的基礎設施實作,如與該業務相關的資料訪問邏輯,以及將領域知識持久化的資料庫模型,形成縱向的邏輯邊界,即限界背景關系邊界,

圖片

穩定空間:限界背景關系必須防止和減少外部變化帶來的影響,

獨立進化:指減少限界背景關系內部變化對外界產生的影響,

上述的四個特征可以幫助我們驗證識別出來的限界背景關系,限界背景關系劃分是否合理、職責分配是否合理(最小完備 & 自我履行),是否合理運用背景關系映射的手段隔離外部變化的影響(穩定空間)、是否有合理的封裝,對外提供的介面是否穩定(獨立進化)?

6.2 限界背景關系的識別

6.2.1 按業務維度識別

1. 歸類

按照業務相關性對業務服務進行歸類,業務相關性體現為:

  • 語意相關性:存在相同或相似的領域概念,對應于業務服務描述的名詞,如果不同的業務服務操作了相同或相似的物件,即可認為它們存在語意相關性,
  • 功能相關性:體現領域行為的相關性,業務服務是否服務于同一個業務目標,

2. 歸納

歸納是對歸類后的限界背景關系進行命名,給限界背景關系命名的程序,實際上也是對歸類是否合理的再一次復查,限界背景關系的命名同樣需要遵循單一職責原則,它只能代表唯一的最能體現其特征的領域概念,倘若歸類不合理,命名就會變得困難,這時候我們就需要反思(遵循知識消化回圈)歸類是否合理,并重新設計歸類,

3. 邊界梳理

歸類和歸納之后,限界背景關系的邊界基本已經確定,邊界梳理則是根據限界背景關系特征(最小完備、自我履行、穩定空間和獨立進化)以及子領域進行微調(當然也不排除大調),

為什么需要根據子領域進行限界背景關系邊界的調整?限界背景關系和子領域的關系是什么?

理想的限界背景關系與子領域的關系是一一對應的,上文提到,子領域是領域專家根據領域經驗選擇合適的功能分類策略進行劃分,這個程序不會牽扯對業務服務的分析,體現的是領域專家對行業的洞見和深刻認識,可見獲取子領域是一個自頂向下的程序,而限界背景關系則是對業務服務進行歸類、歸納、梳理和調整,最終形成一個個的邊界,這是一個自下而上的程序,理想情況下,兩者應該是雙向奔赴的,自頂向下得到的子領域和自下而上得到的限界背景關系能夠完美契合!但是,現實哪有這么理想呢!所以一般情況下都需要我們進行調整,力求這兩者能夠一一對應,

這里就再cue一下知識消化回圈,優秀的領域專家劃分出來的子領域,往往能夠實作與限界背景關系的一一對應,這就是經驗的力量!那經驗是怎么來的呢?我認為是領域專家經歷了無數個知識消化回圈之后沉淀下來的,領域專家一開始也是小白,劃分出來的子領域在映射為限界背景關系之后發現不同限界之間可能存在語意重疊,角色在不同限界背景關系之中履行的職責可能很相似,于是他們通過知識消化回圈,不斷調整限界背景關系的邊界,然后又通過限界背景關系調整子領域,慢慢地,穩定、可復用的子領域就被沉淀下來了,因此,識別限界背景關系不是一個單向的程序,而是一個根據子領域調整限界背景關系,然后又根據限界背景關系調整子領域的回圈的程序,
6.2.2 驗證

正交原則

正交性:如果兩個或更多事物中的一個發生變化,不會影響其他事物,這些事物就是正交的,要破壞變化的傳遞性,就要保證每個限界背景關系對外提供的業務服務不能出現雷同,

奧卡姆剃刀原理

“如無必要,勿增物體”,這是避免過度設計的良方,同樣也是我們識別限界背景關系的原則,如果對識別出來的限界背景關系的準確性依然心存疑慮,比較務實的做法是保證限界背景關系具備一定的粗粒度,遵循該原則,意味著當我們沒有尋找到必須切分限界背景關系的必要證據時,就不要增加新的限界背景關系,

6.3 背景關系映射

限界背景關系封裝了分離的業務能力,背景關系映射則建立了限界背景關系之間的關系,背景關系映射提供了各種模式(防腐層、開放主機服務、發布語言、共享內核、合作者、客戶方/供應方、分離方式、遵奉者、大泥球),本質是在控制變化在限界背景關系之間傳遞所產生的影響

下文將提供服務的限界背景關系稱為“上游”背景關系(U 表示),消費服務的限界背景關系稱為“下游”背景關系(D 表示),

6.3.1 防腐層

引入防腐層的目的是為了隔離耦合,防腐層往往位于下游,通過它隔離上游背景關系發生的變化,

圖片
6.3.2 開放主機服務

開放主機服務定義公開服務的協議(亦稱為“服務契約”),包括通信方式、傳遞訊息的格式(協議),讓限界背景關系可以被當做一組服務訪問,開放主機服務也可以視為一種承諾,保證開放的服務不會輕易做出變化,

對于行程內的開放主機服務,稱為本地服務(對應 DDD 中的應用服務),

對于行程間的開放主機服務,成為遠程服務,根據選擇的分布式通信技術的不同,又可以定義出型別不同的遠程服務:

  • 面向服務行為,比如基于 RPC,稱為提供者(Provider);
  • 面向服務資源,比如基于 REST,稱為資源(Resource);
  • 面向事件,比如基于訊息中間件,稱為訂閱者(Subscriber);
  • 面向視圖模型,比如基于 MVC,稱為控制器(Controller);
圖片
6.3.3 發布語言

發布語言是一種公共語言,用于兩個限界背景關系之間的模型轉換,防腐層和開放主機服務都是訪問領域模型時建立的一層包裝,前者針對發起呼叫的下游(通過基礎設施層體現),后者針對回應請求的上游(通過應用層+遠程服務),以避免上下游之間的通信集成將各自的領域模型引入進來,造成彼此之間的強耦合,因此,防腐層和開放主機服務操作的物件都不應該是各自的領域模型,這正是引入發布語言的原因,(對于熟悉云 API 的小伙伴就會發現,其實云 API 根據我們定義的介面生成對應的 Request 物件和 Response 物件,并集成在云 API 的 SDK 中,這些物件就是發布語言),

一般情況下,發布語言根據開放主機服務的服務契約進行定義,

說到這里,我們驚訝地發現防腐層開放主機服務發布語言可以完美聯動!

圖片
6.3.4 共享內核

共享內核指將限界背景關系中的領域模型直接暴露給其他限界背景關系使用,注意,這會削弱了限界背景關系邊界的控制力,上面我們講述的防腐層、開放主機服務以及發布語言無不傳達一種思想,限界背景關系不能直接暴露自己的領域模型或直接訪問其他限界背景關系的領域模型,一定要有隔離層!

但是,在特定的場景下,共享內核不見得不是一種合理的方式,任何軟體設計決策都要考量成本與收益,只有收益高于成本,決策才是合理的,一般對于一些領域通用的值物件是相對穩定的,這些型別通常屬于通用子領域,會被系統中幾乎所有的限界背景關系復用,那么這些領域模型就適合使用共享內核的方式,共享內核的收益不言而喻,而面臨的風險則是共享的領域模型可能產生的變化,

圖片
6.3.5 合作者

合作關系指的是協作的限界背景關系由不同的團隊負責,且這些團隊之間具有要么一起成功,要么一起失敗的強耦合關系,合作者模式要求參與的團隊一起做計劃、一起提交代碼、一起開發和部署,采用持續集成的方式保證兩個限界背景關系的集成度與一致性,避免因為其中一個團隊的修改影響集成點的失敗,

6.3.6 客戶方/供應方

當一個限界背景關系單向地為另一個限界背景關系提供服務時,它們對應的團隊就形成了客戶方/供應方模式,這是最為常見的團隊協作模式,客戶方作為下游團隊,供應方作為上游團隊,二者協作的主要內容包括:

  • 下游團隊對上游團隊提出的服務
  • 上游團隊提供的服務采用什么樣的協議與呼叫方式
  • 下游團隊針對上游服務的測驗策略
  • 上游團隊給下游團隊承諾的交付日期
  • 當上游服務的協議或呼叫方式發生變更時,如何控制變更
6.3.7 分離方式

分離方式的團隊協作模式是指兩個限界背景關系之間沒有一丁點關系,如果此時雙方使用到了相似/相同的領域模型,則可以通過拷貝的方式解決,保證限界背景關系之間的物理隔離!

6.3.8 遵奉者

當上游的限界背景關系處于強勢地位,且上游團隊回應不積極時,我們可以采用遵奉者模式,即下游嚴格遵從上游團隊的模型,以消除復雜的轉換邏輯,

當下游團隊選擇“遵奉”于上游團隊設計的模型時,意味著:

  • 可以直接復用上游背景關系的模型(好的);
  • 減少了兩個限界背景關系之間模型的轉換成本(好的);
  • 使得下游限界背景關系對上游產生了模型上的強依賴(壞的),
6.3.9 大泥球

一定要避免制造大泥球!大泥球的特點:

  • 越來越多的聚合因為不合理的關聯和依賴導致交叉污染;
  • 對大泥球的維護牽一發而動全身;
  • 強調“個人英雄主義”,只有個別“超人”能夠理清邏輯,

6.4 示例-SMS 的限界背景關系及其映射

示例-SMS 的限界背景關系可劃分為:

  • 成績背景關系
  • 課程背景關系
  • 審批背景關系
  • 權限背景關系
  • 郵件背景關系

背景關系映射圖如下所示,

圖片

7.領域建模階段

領域建模階段由領域分析建模,領域設計建模和領域實作建模組成,在正式講解建模活動前,先了解一下什么是模型驅動設計,

7.1 模型驅動設計

模型是一種知識形式,它對知識進行了選擇性的簡化和有意的結構化,從而解決資訊超載的問題,模型便于人們理解資訊的意義,并專注核心問題,

建模程序一般由分析活動設計活動實作活動組成,每一次建模活動都是一次對知識的提煉和轉換,并產生相應的模型,即分析模型設計模型實作模型

建模程序并非是分析、設計和實作單向的前后串行程序,而是相互影響,不斷切換和遞進的關系,模型驅動設計的建模程序是:分析中蘊含了設計,設計中夾帶了實作,甚至實作后還要回溯到設計和分析的一種迭代的螺旋上升的演程序序,

根據分解問題的視角不同,我們日常建立的模型可以大致分為以下三類:

  • 資料模型:將問題空間抽取出來的概念視為資料資訊,在求解程序中關注資料物體的樣式和它們之間的關系,由此建立的模型就是資料模型,
  • 服務模型:將每個問題視為目標系統為客戶端提供的服務,在求解程序就會關注客戶端發起的請求以及服務回傳的回應,由此建立的模型就是服務模型,
  • 領域模型:圍繞問題空間的業務需求,在求解程序中力求提煉出表達領域知識的邏輯概念,由此建立的模型就是領域模型,
7.1.1 領域模型驅動設計

一個優秀的領域模型應該具備以下的特征(我們也可以說具備這些特征的模型就是領域模型):

  • 運用統一語言來表達領域中的概念;
  • 蘊含業務活動和規則等領域知識;
  • 對領域知識進行適度的提煉和抽象;
  • 由一個迭代的演程序序建立;
  • 有助于產品、領域專家和開發同學進行交流,

領域建模階段目的便是建立領域模型,領域模型由領域分析模型領域設計模型以及領域實作模型共同組成,它們也分別是領域分析建模、領域設計建模和領域實作建模三個建模活動的產物,

值得注意的是,領域模型并非由開發團隊單方面輸出的產物,而是由產品、領域專家和開發團隊共同協作的結果,領域專家通過領域模型能夠判斷系統所支持的領域能力,以及由此編排出來的上層業務能力;開發團隊通過領域模型能夠形成基本的代碼框架(包括架構分層,每層需要定義的介面,介面的命名等),同理,領域模型的調整,也意味著領域知識或業務規則的變化,也預示著系統所支持的業務能力和代碼實作同樣需要作出改變,

7.2 領域分析建模

領域分析建模:在限界背景關系內,以“領域”為中心,提煉業務服務中的領域概念,確定領域概念之間的關系,最終形成領域分析模型,領域分析模型描述了各個限界背景關系中的領域概念,以及領域概念之間的關系,

下面講述如何通過“快速建模法”來構建領域分析模型,

7.2.1 名詞建模

找到業務服務中的名詞,在統一語言指導下將其映射為領域概念,

7.2.2 動詞建模

識別動詞并不是為領域模型物件分配職責、定義方法,而是將識別出來的動詞當做一個領域行為,然后看它是否產生了影響管理、法律或財務的程序資料,若存在,則將這些程序資料作為領域概念放到領域分析模型中,注意,這里的程序資料是要求會對企業運營和管理產生影響的資料,比如示例-SMS 系統中老師提交修改申請,就會產生申請單這個程序資料,而請求流水記錄、任務執行記錄都不屬于程序資料,動詞建模通過分析領域行為是否產生程序資料來找到隱藏的領域概念,彌補了名詞建模的不足,

特別地,對于會產生領域事件的動詞,一般可以抽象出一個已完成該動作的狀態,

7.2.3 提取隱式概念

除了“名詞”和“動詞”,概念中其他重要的類別也可以在模型中顯式地表現出來,主要包括:約束規格

約束

約束一般是對領域概念的限制,我們可以將約束條件提取到自己的方法中,并通過方法名顯式地表達約束的含義,比如示例-SMS 中關于 GPA 運算的約束,

圖片

有些時候,約束條件無法用單獨一個方法來輕松表達,抑或約束條件中會使用到與物件職責無關的資訊,那么我們就可以將其提取到一個顯式的物件中,

規格(SPECIFICATION)

很多時候業務規則并不適合作為物體或值物件的職責,而且規則的變化和組合也會掩蓋領域物件的含義,但是,將規則移出領域層則導致領域代碼無法表達模型,此時,我們可以定義規格謂詞形式的顯式值物件),它用于確定物件是否滿足指定的標準,規格將規則保留在領域層,由于規格是一個完備的物件,所以這種設計也能更加清晰地反映模型,

規格一般有如下三種用法:

  • (驗證)驗證物件,檢查它是否能滿足某些標準,比如示例-SMS 中成績物體在修改分數時就需要通過規約判斷當前是否滿足修改的標準;
  • (選擇)從集合中選擇一個符合要求的物件,可以搭配資源庫使用
  • (根據要求來創建)指定在創建新物件時必須滿足某種要求,
圖片

規格由“謂詞”概念演變而來,因此我們可以使用“AND”,“OR”和“NOT”等運算對規格進行組合和修改,比如在 SMS 中,教務員需要查詢流程完結的申請單,我們就可以通過“AND”組合不同的規格進行實作,

圖片
7.2.4 歸納抽象

對于有定語修飾的名詞,要注意分辨它們是型別的差異,還是值的差異,如配送地址和家庭地址,訂單狀態和商品狀態,如果是值的差異,型別相同,應歸并為一個領域概念(如,配送地址和家庭地址);而型別不同,則不能合并(如,訂單狀態和商品狀態),

特別地,當定語修飾的名詞中,定語表示的是不同的限界背景關系,且名詞相同時(即名稱相同、含義不同的領域概念),我們應該盡可能調整命名,確保含義不同的領域概念的名稱不同,以避免不必要的歧義和溝通上的誤解,比如:商品的訂單和庫存的訂單在特定限界背景關系內都可以命名為 order,但是如果把庫存的訂單改為庫存的配送單 delivery 效果會更好,

7.2.5 確認關系

根據業務需求和領域知識,判斷領域概念之間是否存在關聯,且對于 1:N, N:1, M:N 的關聯關系,我們需要判斷是否可以為這些關聯關系定義一個新的型別,比如作品與讀者存在 1:N 的關系,我們可以定義“訂閱”這個概念來描述這種關系,

注意,我們需要盡量避免物件中的雙向關系,即物件 A 關聯物件 B,而物件 B 關聯物件 A,當兩個物件存在雙向關系時,會為管理他們的生命周期帶來額外的復雜度,我們應該規定一個遍歷方向,來表明一個方向的關聯比另一個方向的關聯更有意義且更重要,比如示例 SMS 中,成績會關聯課程(成績實體中包含課程 ID),而課程不會關聯成績,當然,當雙向關系是領域的一個概念時,我們還是應該保留它,

7.2.6 示例-SMS 的領域分析模型

通過名詞建模,動詞建模和歸納抽象后,可提煉出以下領域物件:成績(Result)、績點(gpa)、總成績(total result)、總績點(total gpa)、學年(school year)、學期(semester)、課程(course)、學分(credit)、申請單(application receipt),郵件(mail),排名(rank),申請單狀態(application receipt status)

這些領域物件之間的關系如下圖所示,

7.3 領域設計建模

領域設計建模的核心作業就是設計聚合設計服務,在這之前我們需要先了解一下設計要素(物體、值物件、聚合、工廠、資源庫、領域服務、領域事件),

7.3.1 設計要素

領域驅動設計強調以“領域”為核心驅動力,設計領域模型時應該盡量避免陷入到技術實作的細節約束中,但很多時候我們又不得不去思考一些非領域相關的問題:

  • 領域模型物件在身份上是否存在明確的差別?
  • 領域模型物件的加載以及物件間的關系如何處理?
  • 領域模型物件如何實作資料的持久化?
  • 領域模型物件彼此之間如何做到弱依賴地完成狀態的變更通知?

為了解答上述的四個問題,DDD 提供了很多的設計要素,它們能夠幫助我們在不陷入到具體技術細節的情況下進行領域模型的設計

7.3.1.1 物體

物體的核心三要素:身份標識屬性領域行為

身份標識:身份標識的主要目的是管理物體的生命周期,身份標識可分為:通用型別和領域型別,通用型別 ID 沒有業務含義;而領域型別 ID 則組裝了業務邏輯,建議使用值物件作為領域型別 ID,

屬性:物體的屬性用來說明主體的靜態特征,并持有資料與狀態,屬性分為:原子屬性和組合屬性,組合屬性可以是物體,也可以是值物件,取決于該屬性是否需要身份標識,我們應該盡可能將物體的屬性定義為組合屬性,以便于在物體內部形成各自的抽象層次,

領域行為:體現了物體的動態特征,物體具有的領域行為一般可以分為:

  • 變更狀態的領域行為:變更狀態的領域行為體現的是物體/值物件內部的狀態轉移,對應的方法入參為期望變更的狀態,(有入參,無出參);
  • 自給自足的領域行為:自給自足意味著物體物件只操作了自己的屬性,不外求于別的物件,(無入參);
  • 互為協作的領域行為:需要呼叫者提供必要的資訊,(有入參,有出參);
  • 創建行為:代表了物件在記憶體的從無到有,創建行為由建構式履行,但對于創建行為較為復雜或需要表達領域語意時,我們可以在物體中定義簡單工廠方法,或使用專門的工廠類進行創建,(有出參,且出參為特定物體實體),

7.3.1.2 值物件

一個領域概念到底該用值物件還是物體型別,判斷依據:

  • 業務的參與者對它的相等判斷是依據值還是依據身份標識;
  • 確定物件的屬性值是否會發生變化,如果變化了,究竟是產生一個完全不同的物件,還是維持相同的身份標識;
  • 生命周期的管理,值物件無需進行生命周期管理,

值物件具有不變性,值物件完成創建后,其屬性和狀態就不應該再進行變更了,如果需要更新值物件,則通過創建新的值物件進行替換,

由于值物件的屬性是在其創建的時候就完成傳入的,那么值物件所具有的領域行為大部分情況下都是“自給自足的領域行為”,即入參為空,這些領域行為一般提供以下的能力,

  • 自我驗證:驗證傳入值物件的外部資料是否正確,一般在創建該值物件時進行驗證,
  • 自我組合:當值物件涉及到數值運算時,可以定義相同型別值物件的方法,使值物件具有自我組合能力,比如示例-SMS 中,在統計成績時會涉及學分相加的運算,因此我們可以將相加運算定義為可組合的方法,便于呼叫者使用,
  • 自我運算:根據業務規則對屬性值進行運算的行為,

在進行領域設計建模時,要善于運用值物件而非內建型別去表達細粒度的領域概念,相比于內建型別,值物件的優勢有:

  • 值物件在型別層面就可以表達領域概念,而不僅僅依賴命名;
  • 值物件可以封裝領域行為,進行自我驗證,自我組合,自我運算,

7.3.1.3 聚合

聚合的基本特征:

  • 聚合是包含了物體和值物件的一個邊界,
  • 聚合內包含的物體和值物件形成一棵樹,只有物體才能作為這棵樹的根,
  • 外部物件只允許持有聚合根的參考,以起到邊界控制作用,
  • 聚合作為一個完整的領域概念整體,其內部會維護這個領域概念的完整性,
  • 由聚合根統一對外提供履行該領域概念職責的行為方法,實作內部各個物件之間的行為協作,

7.3.1.4 工廠

聚合中的工廠:一個類或方法只要封裝了聚合物件的創建邏輯,都可以認為是工廠,表現形式如下:

  • 引入專門的聚合工廠(尤其適合需要通過訪問外部資源來完成創建的復雜創建邏輯)
  • 聚合自身擔任工廠(簡單工廠模式)
  • 服務契約物件或裝配器(assembler)擔任工廠(負責將外部請求物件 DTO 轉換為物體)
  • 使用構建者組裝聚合

注意!這里工廠創建的基本單元是聚合,而非物體,注意與物體中的創建行為區分,

7.3.1.5 資源庫

資源庫是對資料訪問的一種業務抽象,用于解耦領域層與外部環境,使領域層變得更為純粹,資源庫可以代表任何可以獲取資源的倉庫,例如網路或其他硬體環境,而不局限于資料庫,

一個聚合對應一個資源庫,領域驅動設計引入資源庫,主要目的是管理聚合的生命周期,資源庫負責聚合記錄的查詢與狀態變更,即“增刪改查”操作,資源庫分離了聚合的領域行為和持久化行為,保證了領域模型物件的業務純粹性,

值得注意的是,資源庫的操作單元是聚合,當我們定義資源庫的介面時,介面的入參應該為聚合的根物體,如果要訪問聚合內的非根物體,也只能通過資源庫獲得整個聚合后,將根物體作為入口,在記憶體中訪問封裝在聚合邊界內的非根物體物件,

資源庫與資料訪問物件(DAO)的區別:

根本區別在于,資料訪問物件在訪問資料時,并無聚合的概念,也就是沒有定義聚合的邊界約束領域模型物件,使得資料訪問物件的操作粒度可以針對領域層的任何模型物件,資料訪問物件(DAO)可以自由地操作物體和值物件,沒有聚合邊界控制的資料訪問,會在不經意間破壞領域概念的完整性,突破聚合不變數的約束,也無法保證聚合物件的獨立訪問與內部資料的一致性,

其次,資源庫是基于領域模型對存盤系統進行的抽象,因此資源庫中的方法命名可以表達領域概念;而資料訪問物件(DAO)是存盤系統對外暴露的抽象,其方法命名更貼合資料庫本身的操作,

**7.3.1.6 領域服務 **

聚合通過聚合根的領域行為對外提供服務,而領域服務則是對聚合根的領域行為的補充,因此,我們應該盡量優先通過聚合根的領域行為來滿足業務服務

那什么場景下我們會需要用到領域服務呢?有如下兩個:

  • 生命周期管理,為了避免領域知識的泄露,應用服務不會直接參考聚合生命周期相關的服務(工廠、資源庫介面),而聚合根物體一般不會依萊澩庫介面,此時就需要領域服務進行組合對外暴露,
  • 依賴外部資源為了保證聚合的穩定性,聚合根物體不會依賴防腐層介面,因此,當聚合對外暴露的服務需要設計外部資源訪問時,就需要通過領域服務來完成,

7.3.1.7 領域事件

領域事件屬于領域層的領域模型物件,由限界背景關系中的聚合發布,感興趣的聚合(同一限界背景關系/不同限界背景關系)可以進行消費,而當一個事件由應用層發布,則該事件為應用事件,

引入領域事件首要目的是更好地跟蹤物體狀態的變更,并在狀態變更時,通過事件訊息的通知完成領域模型物件之間的協作,

領域事件的特征

  • 領域事件代表了領域的概念;
  • 領域事件是已經發生的事實(表示事件的名稱應該是過去時,比如 Committed);
  • 領域事件是不可變的領域物件;
  • 領域事件會基于某個條件而觸發,

領域事件的用途

  • 發布狀態變更;
  • 發布業務流程中的階段性成果;
  • 異步通信,

領域事件應該包含:

  • 身份標識,即事件 ID,為通用型別的身份標識;
  • 事件發生的時間戳,便于記錄和跟蹤;
  • 屬性需要針對訂閱者的需求,在增強事件反向查詢之間進行權衡,增強事件指屬性中包含訂閱者所需的所有資料;反向查詢則是屬性包含事件 ID,當訂閱者需要資料時通過事件 ID 進行反向查詢,
7.3.2 設計聚合

在領域設計模型中,聚合是最小的設計單元,

7.3.2.1 設計的經驗法則

這里有四條經驗法則:

  1. 在聚合邊界內保護業務規則不變性,
  2. 聚合要設計得小巧,
  3. 通過身份識別符號關聯關系其他聚合,
  4. 使用最終一致性更新其他聚合,

下面展開講述法則 1 和法則 3,

法則 1 在聚合邊界內保護業務規則不變性

法則 1 包含了兩個關鍵點:a) 參與維護業務規則不變性的領域概念應該置于同一個聚合內;b) 在任何情況下都要保護業務規則不變性,比如,在 sms 系統中分數和績點具有轉換關系,這是業務規則的不變性,因此這兩個概念被放在了同一個聚合邊界內;當出現老師修改分數的場景時,需要保證績點的換算同時被執行,由于這里績點物件是值物件,不需要關心其生命周期管理的問題,當業務規則涉及到多個物體時,就需要通過本地事務來保證規則不變性(即物體間基于業務規則的資料一致性),

法則 3 通過身份識別符號關聯其他聚合,

注意這里強調了關聯關系,關聯關系會涉及聚合 A 對聚合 B 的生命周期管理的問題,對于這種聚合間的關聯關系,我們通過身份標識建立關聯,而當聚合 A 參考聚合 B,但不需要對聚合 B 進行生命周期管理時,我們認為這是一種依賴關系(比如方法中的入參,而非類中的屬性),對于聚合間的依賴關系,我們可以通過物件參考(聚合根物體的參考)的方式建立依賴,(PS:假設設計之初難以判斷聚合之間到底是關聯關系,還是依賴關系,我們就統一使用身份識別符號作為關系參考即可)

圖片

聚合間的依賴關系通常分為兩種方式

  • 職責的委派:一個聚合作為另一個聚合的方法引數, 就會形成職責的委派,
  • 聚合的創建:一個聚合創建另外一個聚合,就會形成實體化的依賴關系,

7.3.2.2 設計步驟

1. 理順物件圖

分析物件是物體還是值物件,

2. 分解關系薄弱處

聚合本質是一個高內聚的邊界,因此我們可以根據領域物件之間關系的強弱來定義出聚合的邊界,物件間的關系由強到弱可以分為:泛化關系,關聯關系和依賴關系,其中關聯關系和依賴關系在 7.3.2.1 小節已講述,而泛化關系可以理解為是繼承關系(即父子關系),

泛化關系

雖然泛化關系是強耦合關系,但是根據對業務理解的視角不同,會產生不同的設計:

  • 整體視角:呼叫者并不關心特化的子類之間的差異,而是將整個繼承體系視為一個整體,此時應以泛化的父類作為聚合根,
  • 獨立視角:呼叫這只關注具體的特化子類,體現了概念的獨立性,此時應以特化的子類作為獨立的聚合根,

關聯關系

上述提到過,聚合間的關聯關系會涉及聚合 A 對聚合 B 的生命周期管理,這其實是一個比較寬松的約束,那聚合內物體的關聯關系應該是怎么樣的呢?生命周期一致的、共存亡的,當主物體被銷毀時,從物體也隨之會被銷毀,比如商品物體和商品明細物體,而在示例-SMS 中,成績和總成績會被定義為兩個聚合,原因是總成績在成績鎖定后被統計,隨后將不再發生改變,可見兩者不存在上述的共存亡的關聯關系,

PS: 實際上根據關聯關系來區分邊界的方法同樣適用于限界背景關系的邊界劃分,比如示例-SMS 中的課程和成績生命周期不同,先有課程,后有成績;而且成績鎖定后,課程被撤銷也不會對成績有影響,因此就可以定義出課程背景關系和成績上下問,

依賴關系

依賴關系主要體現的是物體間的職責委派和創建行為,可以分到不同的聚合邊界,

3. 調整聚合邊界

根據業務規則調整聚合邊界,為了維護業務規則的不變性,相關的物體應該至于同一個聚合邊界內,

7.3.3 設計服務

這里的服務是對應用服務領域服務領域行為(物體提供的方法)和(資源庫介面、防腐層介面)的統稱,

7.3.3.1 分解任務

業務服務包含若干個組合服務,組合服務包含若干個原子服務領域行為都可以認為是原子服務,

7.3.3.2 分配職責

應用服務:匹配業務服務,提供滿足業務需求的服務介面,應用服務自身并不包含任何領域邏輯,僅負責協調領域模型物件,通過它們的領域能力組合完整一個完整的應用目標,

領域服務:匹配組合服務,執行業務功能,若原子任務為無狀態行為或獨立變化的行為,也可以匹配領域服務,控制多個聚合與埠之間的協作,由它來承擔組合任務的執行,

領域行為:匹配原子服務,提供業務功能的業務實作,強調無狀態和獨立變化,由物體提供,

埠:匹配原子服務,抽象對外資源的訪問,主要的埠包括資源庫介面和防腐層介面,

雖然上述給出了應用服務、領域服務、領域行為和埠與業務服務、組合服務和原子服務的匹配關系,但是對于應用服務、領域服務、領域行為和埠之間的關聯關系卻還不清晰,這里結合書中內容和個人實踐給出一個參考,

應用服務:核心職責是編排聚合間的領域服務,
- 領域服務
- 防腐層介面:當多聚合間領域服務進行協作后需要訪問外部資源,此時相關的防腐層邏輯應該至于應用層,(防腐層是背景關系映射的方式,并非領域模型特有)
- 工廠:特指服務契約物件或裝配器擔任工廠,即將DTO轉換為物體的工廠,
- 領域行為:在上述工廠創建物體后,若只需要呼叫物體的領域行為,而不需要涉及生命周期管理,可直接在應用服務中進行呼叫,

領域服務:細粒度的領域物件可能會把領域層的知識泄露到應用層中,這產生的結果是應用層不得不處理復雜的、細致的互動,從而使得領域知識蔓延到應用層或用戶界面代碼當中,而領域層會丟失這些知識,明智地引入領域層服務有助于在應用層和領域層之間保持一條明確的界限,因此應用層多數情況下也不會直接參考聚合的領域行為,
- 工廠
- 領域行為
- 防腐層介面:聚合內需要依賴外部資源,則將防腐邏輯收攏在領域服務中,
- 資源庫介面

領域行為:不要關聯資源庫和防腐層介面,
7.3.4 示例-SMS 的領域設計模型

聚合設計

圖片

服務設計

下面只羅列非查詢類的服務設計,

圖片
圖片
圖片

7.4 領域實作建模

領域實作建模關注的并非是如何進行代碼實作,而是如何驗證代碼實作的正確性,保證實作的高質量

7.4.1 領域模型與測驗金字塔

領域模型中的服務包括了應用服務領域服務領域行為,其中通過 Provider(面向服務行為)、Resource(面向服務資源)、Subscriber(面向事件)、Controller(面向視圖模型)對外進行暴露的,我們稱為遠程服務

領域模型中的服務與測驗金字塔的關系如下圖所示,

圖片
7.4.2 測驗驅動開發

領域實作建模提倡的是測驗驅動開發的編程思想,即要求開發者在進行邏輯實作前,優先進行測驗用例的撰寫,站在呼叫者角度而非實作者角度去思考介面,

在上述測驗金字塔中,開發者需要關注的是單元測驗(不依賴任何外部資源的測驗就是單元測驗),在領域設計建模階段,我們對業務服務/應用服務進行分解,定義出了領域行為和領域服務,對于領域行為,由于其不依賴外部資源,因此我們可以直接撰寫單元測驗;而對于領域服務,其可能會通過埠訪問外部資源,此時我們需要對埠進行 mock,以隔離外部資源對領域邏輯驗證的干擾,特別地,單元測驗一定要覆寫所有對業務規則的驗證,這是保證領域行為和領域服務正確性的基礎,

單元測驗編碼規范:

  • 測驗類的命名應與被測驗類保持一致,為“被測類名稱+Test 后綴”,
  • 測驗方法表達業務或業務規則為目的,
  • 測驗方法體遵循 Given-When-Then 模式,Given: 為要測驗的方法提供準備,包括創建被測驗物件,為呼叫方法準備輸入引數實參等;When: 呼叫被測驗的方法,遵循單一職責原則,在一個測驗方法的 When 部分,應該只有一條陳述句對被測方法進行呼叫;Then: 對被測方法呼叫后的結果進行預期驗證,

8.分層架構與代碼骨架

8.1 分層架構

圖片

代碼架構分層是經典 DDD 四層:用戶介面層應用層領域層基礎設施層

需要注意的的地方是:

  • 用戶介面層根據通信方式的不同,區分開了 Provider(面向服務行為)、Subscriber(面向事件)、Controller(面向視圖模型&資源) 、Task(面向策略/定時任務),
  • 基礎設施層單獨劃分了 infranstructure-impl 模塊,為了保證領域層的純潔性,DDD 通過依賴倒置把訪問外部系統(資料庫,第三方系統)的服務的實作都下放到了基礎設施層,而 infranstructure-impl 模塊 則是對這些實作進行了歸集,這樣做的好處有兩個:第一,依賴關系明確,(infransturcture-impl —> domain,application), (interface、application、domain —> infranstructure);第二,拆分服務更便捷,當我們需要部分領域獨立拆分出來的時候,在實作層面就只需要關注 infransturcture-impl 模塊 即可,
  • Infranstructure-impl 模塊依賴應用層的原因是應用層可能會抽象出防腐層介面,需要 infranstruct-impl 為其提供實作,

8.2 代碼骨架

8.2.1 用戶介面層

用戶介面層的核心職能:協議轉換和適配、鑒權、引數校驗和例外處理,

├── controller                             //面向視圖模型&資源
│   ├── ResultController.java
│   ├── assembler                         // 裝配器,將VO轉換為DTO
│   │   └── ResultAssembler.java
│   └── vo                                // VO(View Object)物件
│       ├── EnterResultRequest.java
│       └── ResponseVO.java
├── provider                               // 面向服務行為
├── subscriber                             // 面向事件
└── task                                   // 面向策略
    └── TotalResultTask.java
8.2.2 應用層

應用層的核心職能:編排領域服務、事務管理、發布應用事件,

├── assembler                              // 裝配器,將DTO轉換為DO
│   ├── ResultAssembler.java
│   └── TotalResultAssembler.java
├── dto                                    // DTO(Data Transfer Object)物件
│   ├── cmd                                // 命令相關的DTO物件
│   │   ├── ComputeTotalResultCmd.java
│   │   ├── EnterResultCmd.java
│   │   └── ModifyResultCmd.java
│   ├── event                             // 應用事件相關的DTO物件, subscriber負責接收
│   └── qry                               // 查詢相關的DTO物件
└── service                                // 應用服務
    ├── ResultApplicationService.java
    ├── event                              // 應用事件,用于發布
    └── adapter                            // 防腐層配接器介面
8.2.3 領域層

代碼組織以聚合為基本單元,

├── result                                 // 成績聚合
│   ├── entity                            // 成績聚合內的物體
│   │   └── Result.java
│   ├── service                           // 領域服務
│   │   ├── ResultDomainService.java
│   │   ├── event                         // 領域事件
│   │   ├── adapter                       // 防腐層配接器介面
│   │   ├── factory                       // 工廠
│   │   └── repository                    // 資源庫
│   │       └── ResultRepository.java
│   └── valueobject                        // 成績聚合的值物件
│       ├── GPA.java
│       ├── ResultUK.java
│       ├── SchoolYear.java
│       └── Semester.java
└── totalresult                             // 總成績聚合
    ├── ... 這段有點長,其代碼結構與成績聚合一致,因此省略 ...
8.2.4 基礎設施實作層

該層主要提供領域層介面(資源庫、防腐層介面)和應用層介面(防腐層介面)的實作,

代碼組織基本以聚合為基本單元,對于應用層的防腐層介面,則直接以 application 作為包名組織,

├── application                                  // 應用層相關實作
│   └── adapter                                 // 防腐層配接器介面實作
│       ├── facade                              // 外觀介面
│       └── translator                          // 轉換器,DO -> DTO
├── result                                       // 成績聚合相關實作
│   ├── adapter
│   │   ├── facade
│   │   └── translator
│   └── repository                              // 成績聚合資源庫介面實作
│       └── ResultRepositoryImpl.java
└── totalresult                                  // 總成績聚合相關實作
    ├── adapter
    │   ├── CourseAdapterImpl.java
    │   ├── facade
    │   └── translator
    └── repository
        └── TotalResultRepositoryImpl.java

9.雜談

9.1 DDD 與微服務

微服務拆解指的是把一個單體服務拆分為粒度“足夠小”的多個服務,而這里的“足夠小”是一個主觀的,沒有任何標準的定義,盡管如此,我們對“”這個詞還是有一些基本要求的:足夠內聚,足夠獨立,足夠完備,這才使得拆分出來的微服務收益大于投入,試想如果一個微服務提供的業務功能會牽扯到與其他眾多微服務的協作,那豈不是芭比 Q 了,

而上述我們對微服務的基本要求,實際上與限界背景關系的特征(最小完備,自我履行,穩定空間,獨立進化)不謀而合,因此,我們可以把限界背景關系映射為微服務,我在日常實踐中,都是將限界背景關系和微服務的關系進行一一對應的,但這不是絕對的!限界背景關系是站在領域角度給出的邏輯邊界,而微服務的設計往往還要考慮物理邊界,以及實際的質量需求(性能,可用性,安全性等),比如當我們采用的是 CQRS 架構,領域模型會被分為命令模型和查詢模型,雖然它們同屬一個限界背景關系,但是它們往往是物理隔離的,因此,限界背景關系只能作為微服務拆分的指導,而拆分程序中需要考慮質量需求,架構設計等技術因素,

圖片

9.2 事務

9.2.1 本地事務

上文在提及限界背景關系識別和聚合設計的時候其實都提到需要考慮事務屬性,即需要通過本地事務來保證業務規則的不變性/一致性,這里我們會疑惑的是:誰來承擔管理事務的職責?事務管理的邊界是什么?

應用層承擔管理事務的職責

事務本質是一種技術手段,而領域模型本身與技術無關,因此事務應該由應用層負責管理,

事務管理的邊界是聚合,有時限界背景關系也可以

資源庫操作的基本單元是聚合,因此事務管理的邊界是聚合便是自然而然得出的結論,這里需要考慮的是當需要保證事務屬性的不僅僅只有資源庫操作,還包括發布領域事件時(即保證聚合落庫和事件發布的原子性),我們可能需要采用可靠事件模式,即通過把領域事件落庫事件表來表示事件的發布,此時應用層在管理事務時就沒什么心智負擔了,當然,采用可靠事件模式實際是限制了領域模型的實作,也算是技術對領域模型的一種入侵吧,但相比于解放應用層而言,應該是利大于弊,

我們也知道,應用層的核心職責是負責編排和協調不同聚合的領域服務,而應用層又負責事務管理,自然我們能推到出事務管理的邊界是多個聚合(即限界背景關系),但這里有兩個關注點:

a)一般是出于質量需求(性能會好一些,時效性更高一些);

b)同一個限界背景關系內的多個聚合共享一個 DB,

9.2.2 Saga 事務

為了避免耦合,DDD 主張通過柔性事務來保證跨聚合、跨限界背景關系的最終一致性,而目前業界比較主流的應用是 Saga 模式:通過使用異步訊息來協調一系列本地事務,從而維度多個服務之間的資料一致性,而另一個非常著名的柔性事務方案 TCC 為啥沒有 Saga 契合呢?

TCC 共分為三個階段:

  1. Try 階段:準備階段,對資源進行鎖定或預留;
  2. Confirm 階段:提交階段,執行實際的操作;
  3. Cancel 階段:補償階段,任意執行的操作出錯了,就需要執行補償,即釋放 Try 階段預留的資源,

可以看到 TCC 實際對領域模型的侵入是比較大的:

a)TCC 要求領域模型設計時,定義相關的屬性以支持資源鎖定/預留的問題;

b)TCC 對服務介面定義做出了要求,領域模型需要提供 Try,Confirm 和 Cancel 相應的領域服務,

Saga 模式并不要求其對資源進行鎖定/預留,而其補償操作也是通過執行操作的逆操作來完成(比如支付的逆操作是退款),而大部分情況下,完整的領域模型都會對外提供操作及其逆操作,

 

10. 參考

  1. 《解耦-領域驅動設計》
  2. 《領域驅動設計:軟體核心復雜性應對之道》
  3. 《實作領域驅動設計》
  4. 《微服務架構設計模式》
  5. 極客時間《DDD 實戰課》
  6. 極客時間《如何落地業務建模》
  7. 《領域驅動設計精粹》

 

作者:faryrong

本文來自博客園,作者:古道輕風,轉載請注明原文鏈接:https://www.cnblogs.com/88223100/p/10000-word-long-text-to-help-you-get-started-with-software-domain-driven-design-DDD.html

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/542303.html

標籤:領域驅動設計

上一篇:學習筆記——Tomcat中的結點(Server、Service、Connector、Container、Engine、Host、Context);Tomcat啟動-startup.bat

下一篇:讀編程與型別系統筆記11_高級型別及其他

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more