万字长文助你上手软件领域驱动设计 DDD
作者:faryrong,騰訊 CSIG 后臺開發工程師
最近看了一本書《解構-領域驅動設計》,書中提出了領域驅動設計統一過程(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 價值需求分析
價值需求分析主要做的三個工作是:
識別利益相關者。利益相關者指的是與目標系統存在利益關系的人、團隊或組織, 可以簡單理解為目標系統的用戶,或與目標系統有直接交互的人、團隊或組織。
明確系統愿景。闡明目標系統要做什么,以及為何要做。
確定系統范圍。確定系統問題空間的邊界,明確系統什么該做,什么不該做。結合目標系統當前狀態和未來狀態進行判斷。當前狀態指的是系統的可用資源,包括業務資源、人力資源,資金資源等;而未來的狀態則由業務目標、組織的戰略規劃和產品規劃共同構成。
并非任何系統都 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 和法則 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.java8.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.java9.雜談
9.1 DDD 與微服務
微服務拆解指的是把一個單體服務拆分為粒度“足夠小”的多個服務,而這里的“足夠小”是一個主觀的,沒有任何標準的定義。盡管如此,我們對“微”這個詞還是有一些基本要求的:足夠內聚,足夠獨立,足夠完備,這才使得拆分出來的微服務收益大于投入,試想如果一個微服務提供的業務功能會牽扯到與其他眾多微服務的協作,那豈不是芭比 Q 了。
而上述我們對微服務的基本要求,實際上與限界上下文的特征(最小完備,自我履行,穩定空間,獨立進化)不謀而合,因此,我們可以把限界上下文映射為微服務。我在日常實踐中,都是將限界上下文和微服務的關系進行一一對應的,但這不是絕對的!限界上下文是站在領域角度給出的邏輯邊界,而微服務的設計往往還要考慮物理邊界,以及實際的質量需求(性能,可用性,安全性等),比如當我們采用的是 CQRS 架構,領域模型會被分為命令模型和查詢模型,雖然它們同屬一個限界上下文,但是它們往往是物理隔離的。因此,限界上下文只能作為微服務拆分的指導,而拆分過程中需要考慮質量需求,架構設計等技術因素。
9.2 事務
9.2.1 本地事務
上文在提及限界上下文識別和聚合設計的時候其實都提到需要考慮事務屬性,即需要通過本地事務來保證業務規則的不變性/一致性。這里我們會疑惑的是:誰來承擔管理事務的職責?事務管理的邊界是什么?
應用層承擔管理事務的職責
事務本質是一種技術手段,而領域模型本身與技術無關,因此事務應該由應用層負責管理。
事務管理的邊界是聚合,有時限界上下文也可以
資源庫操作的基本單元是聚合,因此事務管理的邊界是聚合便是自然而然得出的結論。這里需要考慮的是當需要保證事務屬性的不僅僅只有資源庫操作,還包括發布領域事件時(即保證聚合落庫和事件發布的原子性),我們可能需要采用可靠事件模式,即通過把領域事件落庫事件表來表示事件的發布。此時應用層在管理事務時就沒什么心智負擔了。當然,采用可靠事件模式實際是限制了領域模型的實現,也算是技術對領域模型的一種入侵吧,但相比于解放應用層而言,應該是利大于弊。
我們也知道,應用層的核心職責是負責編排和協調不同聚合的領域服務,而應用層又負責事務管理,自然我們能推到出事務管理的邊界是多個聚合(即限界上下文)。但這里有兩個關注點:
a)一般是出于質量需求(性能會好一些,時效性更高一些);
b)同一個限界上下文內的多個聚合共享一個 DB。
9.2.2 Saga 事務
為了避免耦合,DDD 主張通過柔性事務來保證跨聚合、跨限界上下文的最終一致性。而目前業界比較主流的應用是 Saga 模式:通過使用異步消息來協調一系列本地事務,從而維度多個服務之間的數據一致性。而另一個非常著名的柔性事務方案 TCC 為啥沒有 Saga 契合呢?
TCC 共分為三個階段:
Try 階段:準備階段,對資源進行鎖定或預留;
Confirm 階段:提交階段,執行實際的操作;
Cancel 階段:補償階段,任意執行的操作出錯了,就需要執行補償,即釋放 Try 階段預留的資源。
可以看到 TCC 實際對領域模型的侵入是比較大的:
a)TCC 要求領域模型設計時,定義相關的屬性以支持資源鎖定/預留的問題;
b)TCC 對服務接口定義做出了要求,領域模型需要提供 Try,Confirm 和 Cancel 相應的領域服務。
Saga 模式并不要求其對資源進行鎖定/預留,而其補償操作也是通過執行操作的逆操作來完成(比如支付的逆操作是退款)。而大部分情況下,完整的領域模型都會對外提供操作及其逆操作。
10. 參考
《解耦-領域驅動設計》
《領域驅動設計:軟件核心復雜性應對之道》
《實現領域驅動設計》
《微服務架構設計模式》
極客時間《DDD 實戰課》
極客時間《如何落地業務建模》
《領域驅動設計精粹》
最近其他好文:
深入揭秘 epoll 是如何實現 IO 多路復用的
低代碼是什么?有什么優勢
Go 高性能編程技法
總結
以上是生活随笔為你收集整理的万字长文助你上手软件领域驱动设计 DDD的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 深入揭秘 epoll 是如何实现 IO
- 下一篇: 如何在IDEA中使用git