領域驅動設計:從理論到實踐,一文帶你掌握 DDD!
從 0 到 1,從理論到實踐,全面講解 DDD,需要學習 DDD 的同學,歡迎來戳~~
前言
學習 DDD 一個半月,最開始學習 DDD 的原因是因爲我負責的業務線,涉及的系統非常多,想借鑑領域驅動設計的思想,看後續如何對系統進行重構。在沒有學習 DDD 之前,感覺 DDD 可能屬於那種 “虛頭巴腦” 的東西,學完 DDD 之後,感覺。。。嗯。。。真香!
有了學習的動力,但是沒有實際參與具體的項目,怎麼辦?那就去廣泛涉獵相關的學習資料,剛好公司內部也進行 DDD 系列課程的培訓,就趕緊報名,然後再通過網上的一些課程和博客資源,將 DDD 的基礎知識系統化。
有了理論基礎,沒有實踐,感覺還是很虛,套用馬克思說過的一句話 “實踐是檢驗真理的唯一標準”,所以我想倒騰個 DDD 的 Demo 出來,剛好公司內部有 DDD 腳手架,更慶幸的是,我還找到一個應用到 DDD 的項目,再結合一個博主寫的 DDD Demo,就把這個 Demo,結合實際的項目,通過 DDD 腳手架重構了一版,經過一個多星期的努力,我的 DDD Demo 就誕生了。經過這一番折騰,如果公司內部有項目需要按照 DDD 重構,我想我也可以!
爲了證明該文章沒有注水,下面列一下我的學習資料:
-
小米內部 DDD 系列分享
-
小米內部 DDD 腳手架
-
小米內部授權認證項目(應用 DDD)
-
極客時間歐創新的《DDD 實戰課》
-
掘金 “柏炎” 的 DDD 系列文檔和 DDD Demo
-
美團技術團隊、阿里雲開發社區、網上博客等
DDD Demo 代碼已經上傳到 GitHub 中,大家可以自取:https://github.com/lml200701158/ddd-framework
git clone git@github.com:lml200701158/ddd-framework.git
走進 DDD
爲什麼要用 DDD
-
面向對象設計,數據行爲綁定,告別貧血模型
-
降低複雜度,分而治之
-
優先考慮領域模型,而不是切割數據和行爲
-
準確傳達業務規則,業務優先
-
代碼即設計
-
它通過邊界劃分將複雜業務領域簡單化,幫我們設計出清晰的領域和應用邊界,可以很容易地實現業務和技術統一的架構演進
-
領域知識共享,提升協助效率
-
增加可維護性和可讀性,延長軟件生命週期
-
中臺化的基石
DDD 作用
說到 DDD,繞不開 MVC,在 MVC 三層架構中,我們進行功能開發的之前,拿到需求,解讀需求。往往最先做的一步就是先設計表結構,在逐層設計上層 dao,service,controller。對於產品或者用戶的需求都做了一層自我理解的轉化。
用戶需求在被提出之後經過這麼多層的轉化後,特別是研發需求在數據庫結構這一層轉化後,將業務以主觀臆斷行爲進行了轉化。一旦業務邊界劃分模糊,考慮不全,大量的邏輯補充堆積到了代碼層實現,變得越來越難維護。
-
消除信息不對稱
-
常規 MVC 三層架構中自底向上的設計方式做一個反轉,以業務爲主導,自頂向下的進行業務領域劃分
-
將大的業務需求進行拆分,分而治之
舉個栗子:
說到這裏大家可能還是有點模糊 DDD 與常見的 mvc 架構的區別。這裏以電商訂單場景爲例。假如我們現在要做一個電商訂單下單的需求。涉及到用戶選定商品,下訂單,支付訂單,對用戶下單時的訂單發貨。
MVC 架構裏面,我們常見的做法是在分析好業務需求之後,就開始設計表結構了,訂單表,支付表,商品表等等。然後編寫業務邏輯。這是第一個版本的需求,功能迭代餓了,訂單支付後我可以取消,下單的商品我們退換貨,是不是又需要進行加表,緊跟着對於的實現邏輯也進行修改。功能不斷迭代,代碼就不斷的層層往上疊。
DDD 架構裏面,我們先進行劃分業務邊界。這裏面核心是訂單。那麼訂單就是這個業務領域裏面的聚合邏輯體現。支付,商品信息,地址等等都是圍繞着訂單實體。訂單本身的屬性決定之後,類似於地址只是一個屬性的體現。當你將訂單的領域模型構建好之後,後續的邏輯邊界與倉儲設計也就隨之而來了。
DDD 基礎概念
學習 DDD 前,有很多基礎概念需要掌握:領域、子域、核心域、通用域、支撐域、實體、值對象、聚合、聚合根、通用語言、限界上下文、事件風暴、領域事件、領域服務、應用服務、工廠、資源庫。
這幅圖總結的很全,他把 DDD 劃分不同的層級,最裏層是值、屬性、唯一標識等,這個是最基本的數據單位,但不能直接使用。然後是實體,這個把基礎的數據進行封裝,可以直接使用,在代碼中就是封裝好的一個個實體對象。之後就是領域層,它按照業務劃分爲不同的領域,比如訂單領域、商品領域、支付領域等。最後是應用服務,它對業務邏輯進行編排,也可以理解爲業務層。
領域和子域
在研究和解決業務問題時,DDD 會按照一定的規則將業務領域進行細分,當領域細分到一定的程度後,DDD 會將問題範圍限定在特定的邊界內,在這個邊界內建立領域模型,進而用代碼實現該領域模型,解決相應的業務問題。簡言之,DDD 的領域就是這個邊界內要解決的業務問題域。
領域可以進一步劃分爲子領域。我們把劃分出來的多個子領域稱爲子域,每個子域對應一個更小的問題域或更小的業務範圍。
領域的核心思想就是將問題域逐級細分,來降低業務理解和系統實現的複雜度。通過領域細分,逐步縮小服務需要解決的問題域,構建合適的領域模型。
舉個例子:
保險領域,我們可以把保險細分爲承保、收付、再保以及理賠等子域,而承保子域還可以繼續細分爲投保、保全(壽險)、批改(財險)等子子域。
核心域、通用域和支撐域
子域可以根據重要程度和功能屬性劃分爲如下:
-
核心域:決定產品和公司核心競爭力的子域,它是業務成功的主要因素和公司的核心競爭力。
-
通用域:沒有太多個性化的訴求,同時被多個子域使用的通用功能的子域。
-
支撐域:但既不包含決定產品和公司核心競爭力的功能,也不包含通用功能的子域。
核心域、支撐域和通用域的主要目標是:通過領域劃分,區分不同子域在公司內的不同功能屬性和重要性,從而公司可對不同子域採取不同的資源投入和建設策略,其關注度也會不一樣。
很多公司的業務,表面看上去相似,但商業模式和戰略方向是存在很大差異的,因此公司的關注點會不一樣,在劃分核心域、通用域和支撐域時,其結果也會出現非常大的差異。
比如同樣都是電商平臺的淘寶、天貓、京東和蘇寧易購,他們的商業模式是不同的。淘寶是 C2C 網站,個人賣家對個人買家,而天貓、京東和蘇寧易購則是 B2C 網站,是公司賣家對個人買家。即便是蘇寧易購與京東都是 B2C 的模式,蘇寧易購是典型的傳統線下賣場轉型成爲電商,京東則是直營加部分平臺模式。因此,在公司建立領域模型時,我們就要結合公司戰略重點和商業模式,重點關注核心域。
通用語言和限界上下文
-
通用語言:就是能夠簡單、清晰、準確描述業務涵義和規則的語言。
-
限界上下文:用來封裝通用語言和領域對象,提供上下文環境,保證在領域之內的一些術語、業務相關對象等(通用語言)有一個確切的含義,沒有二義性。
通用語言
通用語言是團隊統一的語言,不管你在團隊中承擔什麼角色,在同一個領域的軟件生命週期裏都使用統一的語言進行交流。那麼,通用語言的價值也就很明瞭,它可以解決交流障礙這個問題,使領域專家和開發人員能夠協同合作,從而確保業務需求的正確表達。
這個通用語言到場景落地,大家可能還很模糊,其實就是把領域對象、屬性、代碼模型對象等,通過代碼和文字建立映射關係,可以通過 Excel 記錄這個關係,這樣研發可以通過代碼知道這個含義,產品或者業務方可以通過文字知道這個含義,溝通起來就不會有歧義,說的簡單一點,其實就是統一產品和研發的話術。
直接看下面這幅圖(來源於極客時間歐創新的 DDD 實戰課):
限界上下文
通用語言也有它的上下文環境,爲了避免同樣的概念或語義在不同的上下文環境中產生歧義,DDD 在戰略設計上提出了 “限界上下文” 這個概念,用來確定語義所在的領域邊界。
限界上下文是一個顯式的語義和語境上的邊界,領域模型便存在於邊界之內。邊界內,通用語言中的所有術語和詞組都有特定的含義。把限界上下文拆解開看,限界就是領域的邊界,而上下文則是語義環境。通過領域的限界上下文,我們就可以在統一的領域邊界內用統一的語言進行交流。
實體和值對象
-
實體 = 唯一身份標識 + 可變性【狀態 + 行爲】
-
值對象 = 將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念。
實體
DDD 中要求實體是唯一的且可持續變化的。意思是說在實體的生命週期內,無論其如何變化,其仍舊是同一個實體。唯一性由唯一的身份標識來決定的。可變性也正反映了實體本身的狀態和行爲。
實體以 DO(領域對象)的形式存在,每個實體對象都有唯一的 ID。我們可以對一個實體對象進行多次修改,修改後的數據和原來的數據可能會大不相同。但是,由於它們擁有相同的 ID,它們依然是同一個實體。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標識,不管這個商品的數據如何變化,商品的 ID 一直保持不變,它始終是同一個商品。
值對象
當你只關心某個對象的屬性時,該對象便可作爲一個值對象。 我們需要將值對象看成不變對象,不要給它任何身份標識,還應該儘量避免像實體對象一樣的複雜性。
還是舉個訂單的例子,訂單是一個實體,裏面包含地址,這個地址可以只通過屬性嵌入的方式形成的訂單實體對象,也可以將地址通過 json 序列化一個 string 類型的數據,存到 DB 的一個字段中,那麼這個 Json 串就是一個值對象,是不是很好理解?下面給個簡單的圖(同樣是源於極客時間歐創新的 DDD 實戰課):
聚合和聚合根
聚合
聚合:我們把一些關聯性極強、生命週期一致的實體、值對象放到一個聚合裏。聚合是領域對象的顯式分組,旨在支持領域模型的行爲和不變性,同時充當一致性和事務性邊界。
聚合有一個聚合根和上下文邊界,這個邊界根據業務單一職責和高內聚原則,定義了聚合內部應該包含哪些實體和值對象,而聚合之間的邊界是松耦合的。按照這種方式設計出來的服務很自然就是 “高內聚、低耦合” 的。
聚合在 DDD 分層架構裏屬於領域層,領域層包含了多個聚合,共同實現核心業務邏輯。跨多個實體的業務邏輯通過領域服務來實現,跨多個聚合的業務邏輯通過應用服務來實現。比如有的業務場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業務邏輯用領域服務來實現;而有的業務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時你就可以用應用服務來組合這兩個服務。
聚合根
如果把聚合比作組織,那聚合根就是這個組織的負責人。聚合根也稱爲根實體,它不僅是實體,還是聚合的管理者。
-
首先它作爲實體本身,擁有實體的屬性和業務行爲,實現自身的業務邏輯。
-
其次它作爲聚合的管理者,在聚合內部負責協調實體和值對象按照固定的業務規則協同完成共同的業務邏輯。
-
最後在聚合之間,它還是聚合對外的接口人,以聚合根 ID 關聯的方式接受外部任務和請求,在上下文內實現聚合之間的業務協同。也就是說,聚合之間通過聚合根 ID 關聯引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導航到聚合內部實體,外部對象不能直接訪問聚合內實體。
上面講的還是有些抽象,下面看一個圖就能很好理解(同樣是源於極客時間歐創新的 DDD 實戰課):
簡單概括一下:
-
通過事件風暴(我理解就是頭腦風暴,不過我們一般都是先通過個人理解,然後再和相關核心同學進行溝通),得到實體和值對象;
-
將這些實體和值對象聚合爲 “投保聚合” 和“客戶聚合”,其中 “投保單” 和“客戶”是兩者的聚合根;
-
找出與聚合根 “投保單” 和“客戶”關聯的所有緊密依賴的實體和值對象;
-
在聚合內根據聚合根、實體和值對象的依賴關係,畫出對象的引用和依賴模型。
領域服務和應用服務
領域服務
當一些邏輯不屬於某個實體時,可以把這些邏輯單獨拿出來放到領域服務中,理想的情況是沒有領域服務,如果領域服務使用不恰當,慢慢又演化回了以前邏輯都在 service 層的局面。
可以使用領域服務的情況:
-
執行一個顯著的業務操作
-
對領域對象進行轉換
-
以多個領域對象作爲輸入參數進行計算,結果產生一個值對象
應用服務
應用層作爲展現層與領域層的橋樑,是用來表達用例和用戶故事的主要手段。
應用層通過應用服務接口來暴露系統的全部功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委託給一個或多個領域對象來實現,它本身只負責處理業務用例的執行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的複雜性及其內部實現機制。
應用層相對來說是較 “薄” 的一層,除了定義應用服務之外,在該層我們可以進行安全認證,權限校驗,持久化事務控制,或者向其他系統發生基於事件的消息通知,另外還可以用於創建郵件以發送給客戶等。
領域事件
領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理。
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或希望被通知的事情,或與其他模型對象中的狀態更改相關聯,下面簡單說明領域事件:
-
事件發佈:構建一個事件,需要唯一標識,然後發佈;
-
事件存儲:發佈事件前需要存儲,因爲接收後的事建也會存儲,可用於重試或對賬等;
-
事件分發:服務內直接發佈給訂閱者,服務外需要藉助消息中間件,比如 Kafka,RabbitMQ 等;
-
事件處理:先將事件存儲,然後再處理。
比如下訂單後,給用戶增長積分與贈送優惠券的需求。如果使用瀑布流的方式寫代碼。一個個邏輯調用,那麼不同用戶,贈送的東西不同,邏輯就會變得又臭又長。這裏的比較好的方式是,用戶下訂單成功後,發佈領域事件,積分聚合與優惠券聚合監聽訂單發佈的領域事件進行處理。
資源庫【倉儲】
倉儲介於領域模型和數據模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和數據模型,以便我們關注於領域模型而不需要考慮如何進行持久化。
我們將暫時不使用的領域對象從內存中持久化存儲到磁盤中。當日後需要再次使用這個領域對象時,根據 key 值到數據庫查找到這條記錄,然後將其恢復成領域對象,應用程序就可以繼續使用它了,這就是領域對象持久化存儲的設計思想。
DDD 分層
DDD 分層架構
嚴格分層架構:某層只能與直接位於的下層發生耦合。
鬆散分層架構:允許上層與任意下層發生耦合。
在領域驅動設計(DDD)中採用的是鬆散分層架構,層間關係不那麼嚴格。每層都可能使用它下面所有層的服務,而不僅僅是下一層的服務。每層都可能是半透明的,這意味着有些服務只對上一層可見,而有些服務對上面的所有層都可見。
分層的作用,從上往下:
-
用戶交互層:web 請求,rpc 請求,mq 消息等外部輸入均被視爲外部輸入的請求,可能修改到內部的業務數據。
-
業務應用層:與 MVC 中的 service 不同的不是,service 中存儲着大量業務邏輯。但在應用服務的實現中(以功能點爲維度),它負責編排、轉發、校驗等。(在設計和開發時,不要將本該放在領域層的業務邏輯放到應用層中實現。因爲龐大的應用層會使領域模型失焦,時間一長你的服務就會演化爲傳統的三層架構,業務邏輯會變得混亂。)
-
領域層:或稱爲模型層,系統的核心,負責表達業務概念,業務狀態信息以及業務規則。即包含了該領域(問題域)所有複雜的業務知識抽象和規則定義。該層主要精力要放在領域對象分析上,可以從實體,值對象,聚合(聚合根),領域服務,領域事件,倉儲,工廠等方面入手。
-
基礎設施層:主要有 2 方面內容,一是爲領域模型提供持久化機制,當軟件需要持久化能力時候才需要進行規劃;一是對其他層提供通用的技術支持能力,如消息通信,通用工具,配置等的實現。
應用服務層直接調用基礎設施層的一條線,這條線是什麼意思呢?領域模型的建立是爲了控制對於數據的增刪改的業務邊界,至於數據查詢,不同的報表,不同的頁面需要展示的數據聚合不具備強業務領域,因此常見的會使用 CQRS 方式進行查詢邏輯的處理。其它的直接調用,原理類同。
各層數據轉換
每一層都有自己特定的數據,可以做如下區分:
-
VO(View Object):視圖對象,主要對應界面顯示的數據對象。對於一個 WEB 頁面,或者 SWT、SWING 的一個界面,用一個 VO 對象對應整個界面的值。
-
DTO(Data Transfer Object):數據傳輸對象,主要用於遠程調用等需要大量傳輸對象的地方。比如我們一張表有 100 個字段,那麼對應的 PO 就有 100 個屬性。但是我們界面上只要顯示 10 個字段,客戶端用 WEB service 來獲取數據,沒有必要把整個 PO 對象傳遞到客戶端,這時我們就可以用只有這 10 個屬性的 DTO 來傳遞結果到客戶端,這樣也不會暴露服務端表結構. 到達客戶端以後,如果用這個對象來對應界面顯示,那此時它的身份就轉爲 VO。在這裏,我泛指用於展示層與服務層之間的數據傳輸對象。
-
DO(Domain Object):領域對象,就是從現實世界中抽象出來的有形或無形的業務實體。
-
PO(Persistent Object):持久化對象,它跟持久層(通常是關係型數據庫)的數據結構形成一一對應的映射關係,如果持久層是關係型數據庫,那麼,數據表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。最形象的理解就是一個 PO 就是數據庫中的一條記錄,好處是可以把一條記錄作爲一個對象處理,可以方便的轉爲其它對象。
各個 O 的區別和具體使用場景,有些 O 是否一定需要,可以參考文章《【領域驅動系列 2】淺析 VO、DTO、DO、PO》
戰略設計 & 戰術設計
這篇文章有 2 個重要的概念一直沒有提,分別爲 “戰略設計” 和“戰術設計”。
戰略設計
戰略設計從業務視角出發,建立業務領域模型,劃分領域邊界,建立通用語言的限界上下文,限界上下文可以作爲微服務設計的參考邊界。
因爲我給的 Demo 非常簡單,所以就直接跳過了戰略設計這個流程,但是實際的項目中,“戰略設計” 需要比較資深的工程師去掌控。
戰略設計主要流程包括:建立統一語言、領域分解、領域建模
戰略設計的工具包括:事件風暴、用例分析、四色建模、領域故事講述,其中 “事件風暴” 是我們最常用的戰略設計工具。
戰術設計
戰術設計從技術視角出發,側重於領域模型的技術實現,完成軟件開發和落地,包括:聚合根、實體、值對象、領域服務、應用服務和資源庫等代碼邏輯的設計和實現。在我們的 Demo 中,就可以看到很多 “戰術設計” 的影子。
因爲文章篇幅原因,戰略設計和戰術設計就不繼續展開,需要學習這塊內容的同學,網上資料和相關書籍也很多,當然也可以私我哈。
是不是感覺這塊內容比較抽象?直接對着 Demo 學習吧,很多東西你就會豁然開朗。
DDD 實戰
項目介紹
-
主要是圍繞用戶、角色和兩者的關係,構建權限分配領域模型。
-
採用 DDD 4 層架構,包括用戶接口層、應用層、領域層和基礎服務層。
-
數據通過 VO、DTO、DO、PO 轉換,進行分層隔離。
-
採用 SpringBoot + MyBatis Plus 框架,存儲用 MySQL。
工程目錄
項目劃分爲用戶接口層、應用層、領域層和基礎服務層,每一層的代碼結構都非常清晰,包括每一層 VO、DTO、DO、PO 的數據定義。對於每一層的公共代碼,比如常量、接口等,都抽離到 ddd-common 中。
./ddd-application // 應用層
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── applicaiton
├── converter
│ └── UserApplicationConverter.java // 類型轉換器
└── impl
└── AuthrizeApplicationServiceImpl.java // 業務邏輯
./ddd-common
├── ddd-common // 通用類庫
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── common
│ ├── exception // 異常
│ │ ├── ServiceException.java
│ │ └── ValidationException.java
│ ├── result // 返回結果集
│ │ ├── BaseResult.javar
│ │ ├── Page.java
│ │ ├── PageResult.java
│ │ └── Result.java
│ └── util // 通用工具
│ ├── GsonUtil.java
│ └── ValidationUtil.java
├── ddd-common-application // 業務層通用模塊
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── applicaiton
│ ├── dto // DTO
│ │ ├── RoleInfoDTO.java
│ │ └── UserRoleDTO.java
│ └── servic // 業務接口
│ └── AuthrizeApplicationService.java
├── ddd-common-domain
│ ├── pom.xml
│ └── src
│ └── main
│ └── java
│ └── com
│ └── ddd
│ └── domain
│ ├── event // 領域事件
│ │ ├── BaseDomainEvent.java
│ │ └── DomainEventPublisher.java
│ └── service // 領域接口
│ └── AuthorizeDomainService.java
└── ddd-common-infra
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── infra
├── domain // DO
│ └── AuthorizeDO.java
├── dto
│ ├── AddressDTO.java
│ ├── RoleDTO.java
│ ├── UnitDTO.java
│ └── UserRoleDTO.java
└── repository
├── UserRepository.java // 領域倉庫
└── mybatis
└── entity // PO
├── BaseUuidEntity.java
├── RolePO.java
├── UserPO.java
└── UserRolePO.java
./ddd-domian // 領域層
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── domain
├── event // 領域事件
│ ├── DomainEventPublisherImpl.java
│ ├── UserCreateEvent.java
│ ├── UserDeleteEvent.java
│ └── UserUpdateEvent.java
└── impl // 領域邏輯
└── AuthorizeDomainServiceImpl.java
./ddd-infra // 基礎服務層
├── pom.xml
└── src
└── main
└── java
└── com
└── ddd
└── infra
├── config
│ └── InfraCoreConfig.java // 掃描Mapper文件
└── repository
├── converter
│ └── UserConverter.java // 類型轉換器
├── impl
│ └── UserRepositoryImpl.java
└── mapper
├── RoleMapper.java
├── UserMapper.java
└── UserRoleMapper.java
./ddd-interface
├── ddd-api // 用戶接口層
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── ddd
│ │ └── api
│ │ ├── DDDFrameworkApiApplication.java // 啓動入口
│ │ ├── converter
│ │ │ └── AuthorizeConverter.java // 類型轉換器
│ │ ├── model
│ │ │ ├── req // 入參 req
│ │ │ │ ├── AuthorizeCreateReq.java
│ │ │ │ └── AuthorizeUpdateReq.java
│ │ │ └── vo // 輸出 VO
│ │ │ └── UserAuthorizeVO.java
│ │ └── web // API
│ │ └── AuthorizeController.java
│ └── resources // 系統配置
│ ├── application.yml
│ └── resources // Sql文件
│ └── init.sql
└── ddd-task
└── pom.xml
./pom.xml
實戰講解
數據庫
包括 3 張表,分別爲用戶、角色和用戶角色表,一個用戶可以擁有多個角色,一個角色可以分配給多個用戶。
create table t_user
(
id bigint auto_increment comment '主鍵' primary key,
user_name varchar(64) null comment '用戶名',
password varchar(255) null comment '密碼',
real_name varchar(64) null comment '真實姓名',
phone bigint null comment '手機號',
province varchar(64) null comment '用戶名',
city varchar(64) null comment '用戶名',
county varchar(64) null comment '用戶名',
unit_id bigint null comment '單位id',
unit_name varchar(64) null comment '單位名稱',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '創建時間',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改時間',
deleted bigint default 0 not null comment '是否刪除,非0爲已刪除'
)comment '用戶表' collate = utf8_bin;
create table t_role
(
id bigint auto_increment comment '主鍵' primary key,
name varchar(256) not null comment '名稱',
code varchar(64) null comment '角色code',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '創建時間',
gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '修改時間',
deleted bigint default 0 not null comment '是否已刪除'
)comment '角色表' charset = utf8;
create table t_user_role (
id bigint auto_increment comment '主鍵id' primary key,
user_id bigint not null comment '用戶id',
role_id bigint not null comment '角色id',
gmt_create datetime default CURRENT_TIMESTAMP not null comment '創建時間',
gmt_modified datetime default CURRENT_TIMESTAMP not null comment '修改時間',
deleted bigint default 0 not null comment '是否已刪除'
)comment '用戶角色關聯表' charset = utf8;
基礎服務層
倉儲(資源庫)介於領域模型和數據模型之間,主要用於聚合的持久化和檢索。它隔離了領域模型和數據模型,以便我們關注於領域模型而不需要考慮如何進行持久化。
比如保存用戶,需要將用戶和角色一起保存,也就是創建用戶的同時,需要新建用戶的角色權限,這個可以直接全部放到倉儲中:
public AuthorizeDO save(AuthorizeDO user) {
UserPO userPo = userConverter.toUserPo(user);
if(Objects.isNull(user.getUserId())){
userMapper.insert(userPo);
user.setUserId(userPo.getId());
} else {
userMapper.updateById(userPo);
userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery()
.eq(UserRolePO::getUserId, user.getUserId()));
}
List<UserRolePO> userRolePos = userConverter.toUserRolePo(user);
userRolePos.forEach(userRoleMapper::insert);
return this.query(user.getUserId());
}
倉儲對外暴露的接口如下:
// 用戶領域倉儲
public interface UserRepository {
// 刪除
void delete(Long userId);
// 查詢
AuthorizeDO query(Long userId);
// 保存
AuthorizeDO save(AuthorizeDO user);
}
基礎服務層不僅僅包括資源庫,與第三方的調用,都需要放到該層,Demo 中沒有該示例,我們可以看一個小米內部具體的實際項目,他把第三方的調用放到了 remote 目錄中:
領域層
聚合 & 聚合根
我們有用戶和角色兩個實體,可以將用戶、角色和兩者關係進行聚合,然後用戶就是聚合根,聚合之後的屬性,我們稱之爲 “權限”。
對於地址 Address,目前是作爲字段屬性存儲到 DB 中,如果對地址無需進行檢索,可以把地址作爲 “值對象” 進行存儲,即把地址序列化爲 Json 存,存儲到 DB 的一個字段中。
public class AuthorizeDO {
// 用戶ID
private Long userId;
// 用戶名
private String userName;
// 真實姓名
private String realName;
// 手機號
private String phone;
// 密碼
private String password;
// 用戶單位
private UnitDTO unit;
// 用戶地址
private AddressDTO address;
// 用戶角色
private List<RoleDTO> roles;
}
領域服務
Demo 中的領域服務比較薄,通過單位 ID 後去獲取單位名稱,構建單位信息:
@Service
public class AuthorizeDomainServiceImpl implements AuthorizeDomainService {
@Override
// 設置單位信息
public void associatedUnit(AuthorizeDO authorizeDO) {
String unitName = "武漢小米";// TODO: 通過第三方獲取
authorizeDO.getUnit().setUnitName(unitName);
}
}
我們其實可以把領域服務再進一步抽象,可以抽象出領域能力,通過這些領域能力去構建應用層邏輯,比如賬號相關的領域能力可以包括授權領域能力、身份認證領域能力等,這樣每個領域能力相對獨立,就不會全部揉到一個文件中,下面是實際項目的領域層截圖:
領域事件
領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理。
這個 Demo 中,對領域事件的處理非常簡單,還是一個應用內部的領域事件,就是每次執行一次具體的操作時,把行爲記錄下來。Demo 中沒有記錄事件的庫表,事件的分發還是同步的方式,所以 Demo 中的領域事件還不完善,後面我會再繼續完善 Demo 中的領域事件,通過 Java 消息機制實現解耦,甚至可以藉助消息隊列,實現異步。
/**
* 領域事件基類
*
* @author louzai
* @since 2021/11/22
*/
@Getter
@Setter
@NoArgsConstructor
public abstract class BaseDomainEvent<T> implements Serializable {
private static final long serialVersionUID = 1465328245048581896L;
/**
* 發生時間
*/
private LocalDateTime occurredOn;
/**
* 領域事件數據
*/
private T data;
public BaseDomainEvent(T data) {
this.data = data;
this.occurredOn = LocalDateTime.now();
}
}
/**
* 用戶新增領域事件
*
* @author louzai
* @since 2021/11/20
*/
public class UserCreateEvent extends BaseDomainEvent<AuthorizeDO> {
public UserCreateEvent(AuthorizeDO user) {
super(user);
}
}
/**
* 領域事件發佈實現類
*
* @author louzai
* @since 2021/11/20
*/
@Component
@Slf4j
public class DomainEventPublisherImpl implements DomainEventPublisher {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void publishEvent(BaseDomainEvent event) {
log.debug("發佈事件,event:{}", GsonUtil.gsonToString(event));
applicationEventPublisher.publishEvent(event);
}
}
應用層
應用層就非常好理解了,只負責簡單的邏輯編排,比如創建用戶授權:
@Transactional(rollbackFor = Exception.class)
public void createUserAuthorize(UserRoleDTO userRoleDTO){
// DTO轉爲DO
AuthorizeDO authorizeDO = userApplicationConverter.toAuthorizeDo(userRoleDTO);
// 關聯單位單位信息
authorizeDomainService.associatedUnit(authorizeDO);
// 存儲用戶
AuthorizeDO saveAuthorizeDO = userRepository.save(authorizeDO);
// 發佈用戶新建的領域事件
domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO));
}
查詢用戶授權信息:
@Override
public UserRoleDTO queryUserAuthorize(Long userId) {
// 查詢用戶授權領域數據
AuthorizeDO authorizeDO = userRepository.query(userId);
if (Objects.isNull(authorizeDO)) {
throw ValidationException.of("UserId is not exist.", null);
}
// DO轉DTO
return userApplicationConverter.toAuthorizeDTO(authorizeDO);
}
細心的同學可以發現,我們應用層和領域層,通過 DTO 和 DO 進行數據轉換。
用戶接口層
最後就是提供 API 接口:
@GetMapping("/query")
public Result<UserAuthorizeVO> query(@RequestParam("userId") Long userId){
UserRoleDTO userRoleDTO = authrizeApplicationService.queryUserAuthorize(userId);
Result<UserAuthorizeVO> result = new Result<>();
result.setData(authorizeConverter.toVO(userRoleDTO));
result.setCode(BaseResult.CODE_SUCCESS);
return result;
}
@PostMapping("/save")
public Result<Object> create(@RequestBody AuthorizeCreateReq authorizeCreateReq){
authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq));
return Result.ok(BaseResult.INSERT_SUCCESS);
}
數據的交互,包括入參、DTO 和 VO,都需要對數據進行轉換。
項目運行
-
新建庫表:通過文件 "ddd-interface/ddd-api/src/main/resources/init.sql" 新建庫表。
-
修改 SQL 配置:修改 "ddd-interface/ddd-api/src/main/resources/application.yml" 的數據庫配置。
-
啓動服務:直接啓動服務即可。
-
測試用例:
-
請求 URL:http://127.0.0.1:8087/api/user/save
-
Post body:{"userName":"louzai","realName":"樓","phone":13123676844,"password":"***","unitId":2,"province":"湖北省","city":"鄂州市","county":"葛店開發區","roles":[{"roleId":2}]}
結語
談談我對 DDD 的理解,我覺得 DDD 不像一門技術,我理解的技術比如高併發、緩存、消息隊列等,DDD 更像是一項軟技能,一種方法論,包含了很多設計理念。
因爲文章篇幅原因,不可能涵蓋 DDD 所有的內容,特別是 “戰略設計” 的部分,基本是一筆帶過,因爲方法論基本都差不多,具體實操需要經驗的積累,但是對於想入門 DDD 的同學,我覺得這篇文章還在值得大家去學習的。
畢竟接觸 DDD 的時間還不長,所以有些知識點理解的不夠深刻,或者有些偏頗,歡迎大家批評指正!
參考文章:
極客時間:https://time.geekbang.org/column/intro/100037301?tab=catalog
一文帶你落地 DDD:https://juejin.cn/post/7004002483601145863 領域驅動設計在互聯網業務開發中的實踐:https://tech.meituan.com/2017/12/22/ddd-in-practice.html
淺析 VO、DTO、DO、PO:https://developer.aliyun.com/article/269676
歡迎大家多多點贊,更多文章,請關注微信公衆號 “樓仔進階之路”,點關注,不迷路~~
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/x4HjK8t6mPAg1vQWa3PrSg