天天看點

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

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

「後端」萬字長文助你上手軟體領域驅動設計 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 各步驟與戰略&戰術設計的關系見下表)。

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

5.全局分析階段

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

5.1 形成統一語言

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

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

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

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

5.2 價值需求分析

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

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

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

5.3 業務需求分析

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

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

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

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

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

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

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

5.3.2 子領域

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

子領域的作用:

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

子領域的分類:

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

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

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

劃分子領域的過程存在很多經驗因素,一個對該行業領域知識了如指掌的領域專家,可以在完成價值需求分析後,結合自身的領域經驗,能夠選擇合适的聚類政策并給出穩定的子領域清單。但,沒有領域經驗也沒有關系!因為根據知識消化循環思路,再經曆多個疊代後收斂出來的子領域劃分也會逐漸合理,逼急領域專家憑經驗得出的子領域劃分,隻是可能需要的時間要長一些。

6.架構映射階段

在架構映射階段,我們需要識别限界上下文,并通過上下文映射表示限界上下文之間的協作關系。

6.1 限界上下文的定義和特征

6.1.1 限界上下文的定義

限界上下文是語義和語境的邊界。在問題空間,統一語言形成了團隊對領域概念的統一表達,子領域形成了領域概念之間的邊界。而在解空間,限界上下文可以看做是統一語言+子領域的融合體,統一語言需要在限界上下文内才具有明确的業務含義。

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

6.1.2 限界上下文的特征

最小完備:限界上下文在履行屬于自己的業務能力時,擁有的領域知識是完整的,無須針對自己的資訊去求助别的限界上下文。

自我履行:限界上下文能夠根據自己擁有的知識來完成業務能力。自我履行展現了限界上下文縱向切分業務能力的特征。

這裡需要強調一下業務子產品(橫向切分)和限界上下文(縱向切分)的差別。業務子產品不具備完整、獨立的業務能力,它沒有按照同一個業務變化的方向進行。而限界上下文是對目标系統架構的縱向切分,切分的依據是從業務進行考慮的領域次元。為了提供完整的業務能力,在根據領域次元進行劃分時,還需要考慮支撐業務能力的基礎設施實作,如與該業務相關的資料通路邏輯,以及将領域知識持久化的資料庫模型,形成縱向的邏輯邊界,即限界上下文邊界。

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

穩定空間:限界上下文必須防止和減少外部變化帶來的影響。

獨立進化:指減少限界上下文内部變化對外界産生的影響。

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

6.2 限界上下文的識别

6.2.1 按業務次元識别

1. 歸類

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

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

2. 歸納

歸納是對歸類後的限界上下文進行命名。給限界上下文命名的過程,實際上也是對歸類是否合理的再一次複查。限界上下文的命名同樣需要遵循單一職責原則,它隻能代表唯一的最能展現其特征的領域概念。倘若歸類不合理,命名就會變得困難,這時候我們就需要反思(遵循知識消化循環)歸類是否合理,并重新設計歸類。

3. 邊界梳理

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

為什麼需要根據子領域進行限界上下文邊界的調整?限界上下文和子領域的關系是什麼?

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

這裡就再cue一下知識消化循環。優秀的領域專家劃分出來的子領域,往往能夠實作與限界上下文的一一對應。這就是經驗的力量!那經驗是怎麼來的呢?我認為是領域專家經曆了無數個知識消化循環之後沉澱下來的。領域專家一開始也是小白,劃分出來的子領域在映射為限界上下文之後發現不同限界之間可能存在語義重疊,角色在不同限界上下文之中履行的職責可能很相似,于是他們通過知識消化循環,不斷調整限界上下文的邊界,然後又通過限界上下文調整子領域。慢慢地,穩定、可複用的子領域就被沉澱下來了。是以,識别限界上下文不是一個單向的過程,而是一個根據子領域調整限界上下文,然後又根據限界上下文調整子領域的循環的過程。
           

6.2.2 驗證

正交原則

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

奧卡姆剃刀原理

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

6.3 上下文映射

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

下文将提供服務的限界上下文稱為“上遊”上下文(U 表示),消費服務的限界上下文稱為“下遊”上下文(D 表示)。

6.3.1 防腐層

引入防腐層的目的是為了隔離耦合。防腐層往往位于下遊,通過它隔離上遊上下文發生的變化。

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

6.3.2 開放主機服務

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

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

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

  • 面向服務行為,比如基于 RPC,稱為提供者(Provider);
  • 面向服務資源,比如基于 REST,稱為資源(Resource);
  • 面向事件,比如基于消息中間件,稱為訂閱者(Subscriber);
  • 面向視圖模型,比如基于 MVC,稱為控制器(Controller);
「後端」萬字長文助你上手軟體領域驅動設計 DDD

6.3.3 釋出語言

釋出語言是一種公共語言,用于兩個限界上下文之間的模型轉換。防腐層和開放主機服務都是通路領域模型時建立的一層包裝,前者針對發起調用的下遊(通過基礎設施層展現),後者針對響應請求的上遊(通過應用層+遠端服務),以避免上下遊之間的通信內建将各自的領域模型引入進來,造成彼此之間的強耦合。是以,防腐層和開放主機服務操作的對象都不應該是各自的領域模型,這正是引入釋出語言的原因。(對于熟悉雲 API 的小夥伴就會發現,其實雲 API 根據我們定義的接口生成對應的 Request 對象和 Response 對象,并內建在雲 API 的 SDK 中,這些對象就是釋出語言)。

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

說到這裡,我們驚訝地發現防腐層,開放主機服務和釋出語言可以完美關聯!

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

6.3.4 共享核心

共享核心指将限界上下文中的領域模型直接暴露給其他限界上下文使用。注意,這會削弱了限界上下文邊界的控制力。上面我們講述的防腐層、開放主機服務以及釋出語言無不傳達一種思想,限界上下文不能直接暴露自己的領域模型或直接通路其他限界上下文的領域模型,一定要有隔離層!

但是,在特定的場景下,共享核心不見得不是一種合理的方式。任何軟體設計決策都要考量成本與收益,隻有收益高于成本,決策才是合理的。一般對于一些領域通用的值對象是相對穩定的,這些類型通常屬于通用子領域,會被系統中幾乎所有的限界上下文複用,那麼這些領域模型就适合使用共享核心的方式。共享核心的收益不言而喻,而面臨的風險則是共享的領域模型可能産生的變化。

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

6.3.5 合作者

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

6.3.6 客戶方/供應方

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

  • 下遊團隊對上遊團隊提出的服務
  • 上遊團隊提供的服務采用什麼樣的協定與調用方式
  • 下遊團隊針對上遊服務的測試政策
  • 上遊團隊給下遊團隊承諾的傳遞日期
  • 當上遊服務的協定或調用方式發生變更時,如何控制變更

6.3.7 分離方式

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

6.3.8 遵奉者

當上遊的限界上下文處于強勢地位,且上遊團隊響應不積極時,我們可以采用遵奉者模式。即下遊嚴格遵從上遊團隊的模型,以消除複雜的轉換邏輯。

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

  • 可以直接複用上遊上下文的模型(好的);
  • 減少了兩個限界上下文之間模型的轉換成本(好的);
  • 使得下遊限界上下文對上遊産生了模型上的強依賴(壞的)。

6.3.9 大泥球

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

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

6.4 示例-SMS 的限界上下文及其映射

示例-SMS 的限界上下文可劃分為:

  • 成績上下文
  • 課程上下文
  • 審批上下文
  • 權限上下文
  • 郵件上下文

上下文映射圖如下所示。

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

7.領域模組化階段

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

7.1 模型驅動設計

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

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

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

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

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

7.1.1 領域模型驅動設計

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

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

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

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

7.2 領域分析模組化

領域分析模組化:在限界上下文内,以“領域”為中心,提煉業務服務中的領域概念,确定領域概念之間的關系,最終形成領域分析模型。領域分析模型描述了各個限界上下文中的領域概念,以及領域概念之間的關系。

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

7.2.1 名詞模組化

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

7.2.2 動詞模組化

識别動詞并不是為領域模型對象配置設定職責、定義方法,而是将識别出來的動詞當做一個領域行為,然後看它是否産生了影響管理、法律或财務的過程資料。若存在,則将這些過程資料作為領域概念放到領域分析模型中。注意,這裡的過程資料是要求會對企業營運和管理産生影響的資料,比如示例-SMS 系統中老師送出修改申請,就會産生申請單這個過程資料,而請求流水記錄、任務執行記錄都不屬于過程資料。動詞模組化通過分析領域行為是否産生過程資料來找到隐藏的領域概念,彌補了名詞模組化的不足。

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

7.2.3 提取隐式概念

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

限制

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

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

有些時候,限制條件無法用單獨一個方法來輕松表達,抑或限制條件中會使用到與對象職責無關的資訊,那麼我們就可以将其提取到一個顯式的對象中。

規格(SPECIFICATION)

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

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

  • (驗證)驗證對象,檢查它是否能滿足某些标準,比如示例-SMS 中成績實體在修改分數時就需要通過規約判斷目前是否滿足修改的标準;
  • (選擇)從集合中選擇一個符合要求的對象,可以搭配資源庫使用;
  • (根據要求來建立)指定在建立新對象時必須滿足某種要求。
「後端」萬字長文助你上手軟體領域驅動設計 DDD

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

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

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)

這些領域對象之間的關系如下圖所示。

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

7.3 領域設計模組化

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

7.3.1 設計要素

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

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

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

7.3.1.1 實體

實體的核心三要素:身份辨別、屬性和領域行為。

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

屬性:實體的屬性用來說明主體的靜态特征,并持有資料與狀态。屬性分為:原子屬性群組合屬性。組合屬性可以是實體,也可以是值對象,取決于該屬性是否需要身份辨別。我們應該盡可能将實體的屬性定義為組合屬性,以便于在實體内部形成各自的抽象層次。

領域行為:展現了實體的動态特征。實體具有的領域行為一般可以分為:

  • 變更狀态的領域行為:變更狀态的領域行為展現的是實體/值對象内部的狀态轉移,對應的方法入參為期望變更的狀态。(有入參,無出參);
  • 自給自足的領域行為:自給自足意味着實體對象隻操作了自己的屬性,不外求于别的對象。(無入參);
  • 互為協作的領域行為:需要調用者提供必要的資訊。(有入參,有出參);
  • 建立行為:代表了對象在記憶體的從無到有。建立行為由構造函數履行,但對于建立行為較為複雜或需要表達領域語義時,我們可以在實體中定義簡單工廠方法,或使用專門的工廠類進行建立。(有出參,且出參為特定實體執行個體)。

7.3.1.2 值對象

一個領域概念到底該用值對象還是實體類型,判斷依據:

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

值對象具有不變性。值對象完成建立後,其屬性和狀态就不應該再進行變更了,如果需要更新值對象,則通過建立新的值對象進行替換。

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

  • 自我驗證:驗證傳入值對象的外部資料是否正确,一般在建立該值對象時進行驗證。
  • 自我組合:當值對象涉及到數值運算時,可以定義相同類型值對象的方法,使值對象具有自我組合能力。比如示例-SMS 中,在統計成績時會涉及學分相加的運算,是以我們可以将相加運算定義為可組合的方法,便于調用者使用。
「後端」萬字長文助你上手軟體領域驅動設計 DDD
「後端」萬字長文助你上手軟體領域驅動設計 DDD
  • 自我運算:根據業務規則對屬性值進行運算的行為。

在進行領域設計模組化時,要善于運用值對象而非内建類型去表達細粒度的領域概念。相比于内建類型,值對象的優勢有:

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

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:假設設計之初難以判斷聚合之間到底是關聯關系,還是依賴關系,我們就統一使用身份辨別符作為關系引用即可)

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

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

  • 職責的委派:一個聚合作為另一個聚合的方法參數, 就會形成職責的委派。
  • 聚合的建立:一個聚合建立另外一個聚合,就會形成執行個體化的依賴關系。

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 的領域設計模型

聚合設計:

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

服務設計:

下面隻羅列非查詢類的服務設計。

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

7.4 領域實作模組化

領域實作模組化關注的并非是如何進行代碼實作,而是如何驗證代碼實作的正确性,保證實作的高品質。

7.4.1 領域模型與測試金字塔

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

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

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

7.4.2 測試驅動開發

領域實作模組化提倡的是測試驅動開發的程式設計思想,即要求開發者在進行邏輯實作前,優先進行測試用例的編寫,站在調用者角度而非實作者角度去思考接口。

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

單元測試編碼規範:

  • 測試類的命名應與被測試類保持一緻,為“被測類名稱+Test 字尾”。
  • 測試方法表達業務或業務規則為目的。
  • 測試方法體遵循 Given-When-Then 模式。Given: 為要測試的方法提供準備,包括建立被測試對象,為調用方法準備輸入參數實參等;When: 調用被測試的方法,遵循單一職責原則,在一個測試方法的 When 部分,應該隻有一條語句對被測方法進行調用;Then: 對被測方法調用後的結果進行預期驗證。

8.分層架構與代碼骨架

8.1 分層架構

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

代碼架構分層是經典 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 架構,領域模型會被分為指令模型和查詢模型,雖然它們同屬一個限界上下文,但是它們往往是實體隔離的。是以,限界上下文隻能作為微服務拆分的指導,而拆分過程中需要考慮品質需求,架構設計等技術因素。

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

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

文章來源:https://mp.weixin.qq.com/s?__biz=MjM5ODYwMjI2MA==&mid=2649769531&idx=1&sn=0f28bb489247a4ef025cd4e4d4da8d8a&chksm=beccd54089bb5c56e3d947dbd15b7447a798b38ac8e6d4d2750fd6370f21ad3a9550dd229472&scene=21#wechat_redirect

繼續閱讀