DDD框架落地實戰(zhàn)
1. 走進 DDD
1.1 為什么要用 DDD ?
- 面向對象設計,數(shù)據(jù)行為綁定,告別貧血模型;
- 降低復雜度,分而治之;
- 優(yōu)先考慮領域模型,而不是切割數(shù)據(jù)和行為;
- 準確傳達業(yè)務規(guī)則,業(yè)務優(yōu)先;
- 代碼即設計;
- 它通過邊界劃分將復雜業(yè)務領域簡單化,幫我們設計出清晰的領域和應用邊界,可以很容易地實現(xiàn)業(yè)務和技術統(tǒng)一的架構演進;
- 領域知識共享,提升協(xié)助效率;
- 增加可維護性和可讀性,延長軟件生命周期;
- 中臺化的基石。
1.2 DDD 作用
說到 DDD,繞不開 MVC,在 MVC 三層架構中,我們進行功能開發(fā)的之前,拿到需求,解讀需求。往往最先做的一步就是先設計表結構,在逐層設計上層 dao,service,controller。對于產(chǎn)品或者用戶的需求都做了一層自我理解的轉化。
用戶需求在被提出之后經(jīng)過這么多層的轉化后,特別是研發(fā)需求在數(shù)據(jù)庫結構這一層轉化后,將業(yè)務以主觀臆斷行為進行了轉化。一旦業(yè)務邊界劃分模糊,考慮不全,大量的邏輯補充堆積到了代碼層實現(xiàn),變得越來越難維護。
假如我們現(xiàn)在要做一個電商訂單下單的需求,涉及到用戶選定商品,下訂單、支付訂單、對用戶下單時的訂單發(fā)貨:
- MVC 架構:我們常見的做法是在分析好業(yè)務需求之后,就開始設計表結構了,訂單表,支付表,商品表等等。然后編寫業(yè)務邏輯。這是第一個版本的需求,功能迭代餓了,訂單支付后我可以取消,下單的商品我們退換貨,是不是又需要進行加表,緊跟著對于的實現(xiàn)邏輯也進行修改。功能不斷迭代,代碼就不斷的層層往上疊。
- DDD 架構:我們先進行劃分業(yè)務邊界。這里面核心是訂單。那么訂單就是這個業(yè)務領域里面的聚合邏輯體現(xiàn)。支付,商品信息,地址等等都是圍繞著訂單實體。訂單本身的屬性決定之后,類似于地址只是一個屬性的體現(xiàn)。當你將訂單的領域模型構建好之后,后續(xù)的邏輯邊界與倉儲設計也就隨之而來了。
DDD 整體作用總結如下:
- 消除信息不對稱;
- 常規(guī) MVC 三層架構中自底向上的設計方式做一個反轉,以業(yè)務為主導,自頂向下的進行業(yè)務領域劃分;
- 將大的業(yè)務需求進行拆分,分而治之。
2. DDD 架構
2.1 DDD 分層架構
嚴格分層架構:某層只能與直接位于的下層發(fā)生耦合。
松散分層架構:允許上層與任意下層發(fā)生耦合。
在領域驅動設計(DDD)中采用的是松散分層架構,層間關系不那么嚴格。每層都可能使用它下面所有層的服務,而不僅僅是下一層的服務。每層都可能是半透明的,這意味著有些服務只對上一層可見,而有些服務對上面的所有層都可見。
分層的作用,從上往下:
- 用戶交互層:web 請求,rpc 請求,mq 消息等外部輸入均被視為外部輸入的請求,可能修改到內部的業(yè)務數(shù)據(jù)。
- 業(yè)務應用層:與 MVC 中的 service 不同的不是,service 中存儲著大量業(yè)務邏輯。但在應用服務的實現(xiàn)中,它負責編排、轉發(fā)、校驗等。
- 領域層:或稱為模型層,系統(tǒng)的核心,負責表達業(yè)務概念,業(yè)務狀態(tài)信息以及業(yè)務規(guī)則。即包含了該領域所有復雜的業(yè)務知識抽象和規(guī)則定義。該層主要精力要放在領域對象分析上,可以從實體,值對象,聚合(聚合根),領域服務,領域事件,倉儲,工廠等方面入手。
- 基礎設施層:主要有 2 方面內容,一是為領域模型提供持久化機制,當軟件需要持久化能力時候才需要進行規(guī)劃;一是對其他層提供通用的技術支持能力,如消息通信,通用工具,配置等的實現(xiàn)。
在設計和開發(fā)時,不要將本該放在領域層的業(yè)務邏輯放到應用層中實現(xiàn),因為龐大的應用層會使領域模型失焦,時間一長你的服務就會演化為傳統(tǒng)的三層架構,業(yè)務邏輯會變得混亂。
2.2 各層數(shù)據(jù)轉換
每一層都有自己特定的數(shù)據(jù),可以做如下區(qū)分:
- VO(View Object):視圖對象,主要對應界面顯示的數(shù)據(jù)對象。對于一個 WEB 頁面,或者 SWT、SWING 的一個界面,用一個 VO 對象對應整個界面的值。
- DTO(Data Transfer Object):數(shù)據(jù)傳輸對象,主要用于遠程調用等需要大量傳輸對象的地方。比如我們一張表有 100 個字段,那么對應的 PO 就有 100 個屬性。但是我們界面上只要顯示 10 個字段,客戶端用 WEB service 來獲取數(shù)據(jù),沒有必要把整個 PO 對象傳遞到客戶端,這時我們就可以用只有這 10 個屬性的 DTO 來傳遞結果到客戶端,這樣也不會暴露服務端表結構。到達客戶端以后,如果用這個對象來對應界面顯示,那此時它的身份就轉為 VO。在這里,我泛指用于展示層與服務層之間的數(shù)據(jù)傳輸對象。
- DO(Domain Object):領域對象,就是從現(xiàn)實世界中抽象出來的有形或無形的業(yè)務實體。
- PO(Persistent Object):持久化對象,它跟持久層(通常是關系型數(shù)據(jù)庫)的數(shù)據(jù)結構形成一一對應的映射關系,如果持久層是關系型數(shù)據(jù)庫,那么,數(shù)據(jù)表中的每個字段(或若干個)就對應 PO 的一個(或若干個)屬性。最形象的理解就是一個 PO 就是數(shù)據(jù)庫中的一條記錄,好處是可以把一條記錄作為一個對象處理,可以方便的轉為其它對象。
3. DDD 基礎
學習 DDD 前,有很多基礎概念需要掌握,這幅圖總結的很全,他把 DDD 劃分不同的層級:
- 最里層是值、屬性、唯一標識等,這個是最基本的數(shù)據(jù)單位,但不能直接使用。
- 然后是實體,這個把基礎的數(shù)據(jù)進行封裝,可以直接使用,在代碼中就是封裝好的一個個實體對象。
- 之后就是領域層,它按照業(yè)務劃分為不同的領域,比如訂單領域、商品領域、支付領域等。
- 最后是應用服務,它對業(yè)務邏輯進行編排,也可以理解為業(yè)務層。
3.1 領域和子域
在研究和解決業(yè)務問題時,DDD 會按照一定的規(guī)則將業(yè)務領域進行細分,當領域細分到一定的程度后,DDD 會將問題范圍限定在特定的邊界內,在這個邊界內建立領域模型,進而用代碼實現(xiàn)該領域模型,解決相應的業(yè)務問題。簡言之,DDD 的領域就是這個邊界內要解決的業(yè)務問題域。
領域可以進一步劃分為子領域。我們把劃分出來的多個子領域稱為子域,每個子域對應一個更小的問題域或更小的業(yè)務范圍。
領域的核心思想就是將問題域逐級細分,來降低業(yè)務理解和系統(tǒng)實現(xiàn)的復雜度。通過領域細分,逐步縮小服務需要解決的問題域,構建合適的領域模型。
舉個簡單的例子,對于保險領域,我們可以把保險細分為承保、收付、再保以及理賠等子域,而承保子域還可以繼續(xù)細分為投保、保全(壽險)、批改(財險)等子子域。
3.2 核心域、通用域和支撐域
子域可以根據(jù)重要程度和功能屬性劃分為如下:
- 核心域:決定產(chǎn)品和公司核心競爭力的子域,它是業(yè)務成功的主要因素和公司的核心競爭力。
- 通用域:沒有太多個性化的訴求,同時被多個子域使用的通用功能的子域。
- 支撐域:但既不包含決定產(chǎn)品和公司核心競爭力的功能,也不包含通用功能的子域。
核心域、支撐域和通用域的主要目標:通過領域劃分,區(qū)分不同子域在公司內的不同功能屬性和重要性,從而公司可對不同子域采取不同的資源投入和建設策略,其關注度也會不一樣。
很多公司的業(yè)務,表面看上去相似,但商業(yè)模式和戰(zhàn)略方向是存在很大差異的,因此公司的關注點會不一樣,在劃分核心域、通用域和支撐域時,其結果也會出現(xiàn)非常大的差異。
比如同樣都是電商平臺的淘寶、天貓、京東和蘇寧易購,他們的商業(yè)模式是不同的。淘寶是 C2C 網(wǎng)站,個人賣家對個人買家,而天貓、京東和蘇寧易購則是 B2C 網(wǎng)站,是公司賣家對個人買家。即便是蘇寧易購與京東都是 B2C 的模式,蘇寧易購是典型的傳統(tǒng)線下賣場轉型成為電商,京東則是直營加部分平臺模式。
因此,在公司建立領域模型時,我們就要結合公司戰(zhàn)略重點和商業(yè)模式,重點關注核心域。
3.3 通用語言和限界上下文
- 通用語言:就是能夠簡單、清晰、準確描述業(yè)務涵義和規(guī)則的語言。
- 限界上下文:用來封裝通用語言和領域對象,提供上下文環(huán)境,保證在領域之內的一些術語、業(yè)務相關對象等(通用語言)有一個確切的含義,沒有二義性。
3.3.1 通用語言
通用語言是團隊統(tǒng)一的語言,不管你在團隊中承擔什么角色,在同一個領域的軟件生命周期里都使用統(tǒng)一的語言進行交流。那么,通用語言的價值也就很明了,它可以解決交流障礙這個問題,使領域專家和開發(fā)人員能夠協(xié)同合作,從而確保業(yè)務需求的正確表達。
這個通用語言到場景落地,大家可能還很模糊,其實就是把領域對象、屬性、代碼模型對象等,通過代碼和文字建立映射關系,可以通過 Excel 記錄這個關系,這樣研發(fā)可以通過代碼知道這個含義,產(chǎn)品或者業(yè)務方可以通過文字知道這個含義,溝通起來就不會有歧義,說的簡單一點,其實就是統(tǒng)一產(chǎn)品和研發(fā)的話術。
直接看下面這幅圖(來源于極客時間歐創(chuàng)新的 DDD 實戰(zhàn)課):
3.3.2 限界上下文
通用語言也有它的上下文環(huán)境,為了避免同樣的概念或語義在不同的上下文環(huán)境中產(chǎn)生歧義,DDD 在戰(zhàn)略設計上提出了“限界上下文”這個概念,用來確定語義所在的領域邊界。
限界上下文是一個顯式的語義和語境上的邊界,領域模型便存在于邊界之內。邊界內,通用語言中的所有術語和詞組都有特定的含義。把限界上下文拆解開看,限界就是領域的邊界,而上下文則是語義環(huán)境。
通過領域的限界上下文,我們就可以在統(tǒng)一的領域邊界內用統(tǒng)一的語言進行交流。
3.4 實體和值對象
3.4.1 實體
實體 = 唯一身份標識 + 可變性【狀態(tài) + 行為】
DDD 中要求實體是唯一的且可持續(xù)變化的。意思是說在實體的生命周期內,無論其如何變化,其仍舊是同一個實體。唯一性由唯一的身份標識來決定的??勺冃砸舱从沉藢嶓w本身的狀態(tài)和行為。
實體以 DO(領域對象)的形式存在,每個實體對象都有唯一的 ID。我們可以對一個實體對象進行多次修改,修改后的數(shù)據(jù)和原來的數(shù)據(jù)可能會大不相同。
但是,由于它們擁有相同的 ID,它們依然是同一個實體。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標識,不管這個商品的數(shù)據(jù)如何變化,商品的 ID 一直保持不變,它始終是同一個商品。
3.4.2 值對象
值對象 = 將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念。
當你只關心某個對象的屬性時,該對象便可作為一個值對象。我們需要將值對象看成不變對象,不要給它任何身份標識,還應該盡量避免像實體對象一樣的復雜性。
還是舉個訂單的例子,訂單是一個實體,里面包含地址,這個地址可以只通過屬性嵌入的方式形成的訂單實體對象,也可以將地址通過 json 序列化一個 string 類型的數(shù)據(jù),存到 DB 的一個字段中,那么這個 Json 串就是一個值對象,是不是很好理解?
下面給個簡單的圖(同樣是源于極客時間歐創(chuàng)新的 DDD 實戰(zhàn)課):
3.5 聚合和聚合根
3.5.1 聚合
聚合:我們把一些關聯(lián)性極強、生命周期一致的實體、值對象放到一個聚合里。聚合是領域對象的顯式分組,旨在支持領域模型的行為和不變性,同時充當一致性和事務性邊界。
聚合有一個聚合根和上下文邊界,這個邊界根據(jù)業(yè)務單一職責和高內聚原則,定義了聚合內部應該包含哪些實體和值對象,而聚合之間的邊界是松耦合的。按照這種方式設計出來的服務很自然就是“高內聚、低耦合”的。
聚合在 DDD 分層架構里屬于領域層,領域層包含了多個聚合,共同實現(xiàn)核心業(yè)務邏輯。跨多個實體的業(yè)務邏輯通過領域服務來實現(xiàn),跨多個聚合的業(yè)務邏輯通過應用服務來實現(xiàn)。
比如有的業(yè)務場景需要同一個聚合的 A 和 B 兩個實體來共同完成,我們就可以將這段業(yè)務邏輯用領域服務來實現(xiàn);而有的業(yè)務邏輯需要聚合 C 和聚合 D 中的兩個服務共同完成,這時你就可以用應用服務來組合這兩個服務。
3.5.2 聚合根
如果把聚合比作組織,那聚合根就是這個組織的負責人。聚合根也稱為根實體,它不僅是實體,還是聚合的管理者。
- 首先它作為實體本身,擁有實體的屬性和業(yè)務行為,實現(xiàn)自身的業(yè)務邏輯。
- 其次它作為聚合的管理者,在聚合內部負責協(xié)調實體和值對象按照固定的業(yè)務規(guī)則協(xié)同完成共同的業(yè)務邏輯。
- 最后在聚合之間,它還是聚合對外的接口人,以聚合根 ID 關聯(lián)的方式接受外部任務和請求,在上下文內實現(xiàn)聚合之間的業(yè)務協(xié)同。也就是說,聚合之間通過聚合根 ID 關聯(lián)引用,如果需要訪問其它聚合的實體,就要先訪問聚合根,再導航到聚合內部實體,外部對象不能直接訪問聚合內實體。
上面講的還是有些抽象,下面看一個圖就能很好理解(同樣是源于極客時間歐創(chuàng)新的 DDD 實戰(zhàn)課):
簡單概括一下:
- 通過事件風暴(我理解就是頭腦風暴,不過我們一般都是先通過個人理解,然后再和相關核心同學進行溝通),得到實體和值對象;
- 將這些實體和值對象聚合為“投保聚合”和“客戶聚合”,其中“投保單”和“客戶”是兩者的聚合根;
- 找出與聚合根“投保單”和“客戶”關聯(lián)的所有緊密依賴的實體和值對象;
- 在聚合內根據(jù)聚合根、實體和值對象的依賴關系,畫出對象的引用和依賴模型。
3.6 領域服務和應用服務
3.6.1 領域服務
當一些邏輯不屬于某個實體時,可以把這些邏輯單獨拿出來放到領域服務中,理想的情況是沒有領域服務,如果領域服務使用不恰當,慢慢又演化回了以前邏輯都在 service 層的局面。
可以使用領域服務的情況:
- 執(zhí)行一個顯著的業(yè)務操作
- 對領域對象進行轉換
- 以多個領域對象作為輸入?yún)?shù)進行計算,結果產(chǎn)生一個值對象
3.6.2 應用服務
應用層作為展現(xiàn)層與領域層的橋梁,是用來表達用例和用戶故事的主要手段。
應用層通過應用服務接口來暴露系統(tǒng)的全部功能。在應用服務的實現(xiàn)中,它負責編排和轉發(fā),它將要實現(xiàn)的功能委托給一個或多個領域對象來實現(xiàn),它本身只負責處理業(yè)務用例的執(zhí)行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的復雜性及其內部實現(xiàn)機制。
應用層相對來說是較“薄”的一層,除了定義應用服務之外,在該層我們可以進行安全認證,權限校驗,持久化事務控制,或者向其他系統(tǒng)發(fā)生基于事件的消息通知,另外還可以用于創(chuàng)建郵件以發(fā)送給客戶等。
3.7 領域事件
領域事件 = 事件發(fā)布 + 事件存儲 + 事件分發(fā) + 事件處理。
領域事件是一個領域模型中極其重要的部分,用來表示領域中發(fā)生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或希望被通知的事情,或與其他模型對象中的狀態(tài)更改相關聯(lián)。
下面簡單說明領域事件:
- 事件發(fā)布:構建一個事件,需要唯一標識,然后發(fā)布;
- 事件存儲:發(fā)布事件前需要存儲,因為接收后的事建也會存儲,可用于重試或對賬等;
- 事件分發(fā):服務內直接發(fā)布給訂閱者,服務外需要借助消息中間件,比如 Kafka,RabbitMQ 等;
- 事件處理:先將事件存儲,然后再處理。
比如下訂單后,給用戶增長積分與贈送優(yōu)惠券的需求。如果使用瀑布流的方式寫代碼。一個個邏輯調用,那么不同用戶,贈送的東西不同,邏輯就會變得又臭又長。
這里的比較好的方式是,用戶下訂單成功后,發(fā)布領域事件,積分聚合與優(yōu)惠券聚合監(jiān)聽訂單發(fā)布的領域事件進行處理。
3.8 資源庫【倉儲】
倉儲介于領域模型和數(shù)據(jù)模型之間,主要用于聚合的持久化和檢索。它隔離了領域模型和數(shù)據(jù)模型,以便我們關注于領域模型而不需要考慮如何進行持久化。
我們將暫時不使用的領域對象從內存中持久化存儲到磁盤中。當日后需要再次使用這個領域對象時,根據(jù) key 值到數(shù)據(jù)庫查找到這條記錄,然后將其恢復成領域對象,應用程序就可以繼續(xù)使用它了,這就是領域對象持久化存儲的設計思想。
是不是感覺這塊內容比較抽象?直接對著 Demo 學習吧,很多東西你就會豁然開朗。
4. DDD 實戰(zhàn)
4.1 項目介紹
- 主要是圍繞用戶、角色和兩者的關系,構建權限分配領域模型。
- 采用 DDD 4 層架構,包括用戶接口層、應用層、領域層和基礎服務層。
- 數(shù)據(jù)通過 VO、DTO、DO、PO 轉換,進行分層隔離。
- 采用 SpringBoot + MyBatis Plus 框架,存儲用 MySQL。
4.2 工程目錄
項目劃分為用戶接口層、應用層、領域層和基礎服務層,每一層的代碼結構都非常清晰,包括每一層 VO、DTO、DO、PO 的數(shù)據(jù)定義,對于每一層的公共代碼,比如常量、接口等,都抽離到 ddd-common 中。
./ddd-application??//?應用層 ├──?pom.xml └──?src ????└──?main ????????└──?java ????????????└──?com ????????????????└──?ddd ????????????????????└──?applicaiton ????????????????????????├──?converter ????????????????????????│???└──?UserApplicationConverter.java?//?類型轉換器 ????????????????????????└──?impl ????????????????????????????└──?AuthrizeApplicationServiceImpl.java?//?業(yè)務邏輯 ./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?//?業(yè)務層通用模塊 │???├──?pom.xml │???└──?src │???????└──?main │???????????└──?java │???????????????└──?com │???????????????????└──?ddd │???????????????????????└──?applicaiton │???????????????????????????├──?dto?//?DTO │???????????????????????????│???├──?RoleInfoDTO.java │???????????????????????????│???└──?UserRoleDTO.java │???????????????????????????└──?servic?//?業(yè)務接口 │???????????????????????????????└──?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?//?入?yún)?req │???????????│???????????????│???│???├──?AuthorizeCreateReq.java │???????????│???????????????│???│???└──?AuthorizeUpdateReq.java │???????????│???????????????│???└──?vo??//?輸出?VO │???????????│???????????????│???????└──?UserAuthorizeVO.java │???????????│???????????????└──?web?????//?API │???????????│???????????????????└──?AuthorizeController.java │???????????└──?resources?//?系統(tǒng)配置 │???????????????├──?application.yml │???????????└──?resources?//?Sql文件 │???????????????└──?init.sql └──?ddd-task ????└──?pom.xml ./pom.xml
4.3 數(shù)據(jù)庫
包括 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?'創(chuàng)建時間', ????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?'創(chuàng)建時間', ????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?'創(chuàng)建時間', ????gmt_modified?datetime?default?CURRENT_TIMESTAMP?not?null?comment?'修改時間', ????deleted??????bigint???default?0?????????????????not?null?comment?'是否已刪除' )comment?'用戶角色關聯(lián)表'?charset?=?utf8;
4.4 基礎服務層
倉儲(資源庫)介于領域模型和數(shù)據(jù)模型之間,主要用于聚合的持久化和檢索。它隔離了領域模型和數(shù)據(jù)模型,以便我們關注于領域模型而不需要考慮如何進行持久化。
比如保存用戶,需要將用戶和角色一起保存,也就是創(chuàng)建用戶的同時,需要新建用戶的角色權限,這個可以直接全部放到倉儲中:
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 目錄中:
4.5 領域層
4.5.1 聚合&聚合根
我們有用戶和角色兩個實體,可以將用戶、角色和兩者關系進行聚合,然后用戶就是聚合根,聚合之后的屬性,我們稱之為“權限”。
對于地址 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; }
4.5.2 領域服務
Demo 中的領域服務比較薄,通過單位 ID 后去獲取單位名稱,構建單位信息:
@Service public?class?AuthorizeDomainServiceImpl?implements?AuthorizeDomainService?{ ????@Override ????//?設置單位信息 ????public?void?associatedUnit(AuthorizeDO?authorizeDO)?{ ????????String?unitName?=?"武漢小米";//?TODO:?通過第三方獲取 ????????authorizeDO.getUnit().setUnitName(unitName); ????} }
我們其實可以把領域服務再進一步抽象,可以抽象出領域能力,通過這些領域能力去構建應用層邏輯,比如賬號相關的領域能力可以包括授權領域能力、身份認證領域能力等,這樣每個領域能力相對獨立,就不會全部揉到一個文件中,下面是實際項目的領域層截圖:
4.5.3 領域事件
領域事件 = 事件發(fā)布 + 事件存儲 + 事件分發(fā) + 事件處理。
這個 Demo 中,對領域事件的處理非常簡單,還是一個應用內部的領域事件,就是每次執(zhí)行一次具體的操作時,把行為記錄下來。
Demo 中沒有記錄事件的庫表,事件的分發(fā)還是同步的方式,所以 Demo 中的領域事件還不完善,后面我會再繼續(xù)完善 Demo 中的領域事件,通過 Java 消息機制實現(xiàn)解耦,甚至可以借助消息隊列,實現(xiàn)異步。
/** ?*?領域事件基類 ?* ?*?@author?louzai ?*?@since?2021/11/22 ?*/ @Getter @Setter @NoArgsConstructor public?abstract?class?BaseDomainEvent<T>?implements?Serializable?{ ????private?static?final?long?serialVersionUID?=?1465328245048581896L; ????/** ?????*?發(fā)生時間 ?????*/ ????private?LocalDateTime?occurredOn; ????/** ?????*?領域事件數(shù)據(jù) ?????*/ ????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); ????} }
/** ?*?領域事件發(fā)布實現(xiàn)類 ?* ?*?@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("發(fā)布事件,event:{}",?GsonUtil.gsonToString(event)); ????????applicationEventPublisher.publishEvent(event); ????} }
4.4 應用層
應用層就非常好理解了,只負責簡單的邏輯編排,比如創(chuàng)建用戶授權:
@Transactional(rollbackFor?=?Exception.class) public?void?createUserAuthorize(UserRoleDTO?userRoleDTO){ ????//?DTO轉為DO ????AuthorizeDO?authorizeDO?=?userApplicationConverter.toAuthorizeDo(userRoleDTO); ????//?關聯(lián)單位單位信息 ????authorizeDomainService.associatedUnit(authorizeDO); ????//?存儲用戶 ????AuthorizeDO?saveAuthorizeDO?=?userRepository.save(authorizeDO); ????//?發(fā)布用戶新建的領域事件 ????domainEventPublisher.publishEvent(new?UserCreateEvent(saveAuthorizeDO)); }
查詢用戶授權信息:
@Override ??public?UserRoleDTO?queryUserAuthorize(Long?userId)?{ ??????//?查詢用戶授權領域數(shù)據(jù) ??????AuthorizeDO?authorizeDO?=?userRepository.query(userId); ??????if?(Objects.isNull(authorizeDO))?{ ??????????throw?ValidationException.of("UserId?is?not?exist.",?null); ??????} ??????//?DO轉DTO ??????return?userApplicationConverter.toAuthorizeDTO(authorizeDO); ??}
細心的同學可以發(fā)現(xiàn),我們應用層和領域層,通過 DTO 和 DO 進行數(shù)據(jù)轉換。
4.5 用戶接口層
最后就是提供 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); }
數(shù)據(jù)的交互,包括入?yún)?、DTO 和 VO,都需要對數(shù)據(jù)進行轉換。
4.6 項目運行
新建庫表:通過文件 "ddd-interface/ddd-api/src/main/resources/init.sql" 新建庫表。
修改 SQL 配置:修改 "ddd-interface/ddd-api/src/main/resources/application.yml" 的數(shù)據(jù)庫配置。
啟動服務:直接啟動服務即可。
測試用例:
請求 URL:http://127.0.0.1:8087/api/user/save
Post body:{"userName":"louzai","realName":"樓","phone":13123676844,"password":"***","unitId":2,"province":"湖北省","city":"鄂州市","county":"葛店開發(fā)區(qū)","roles":[{"roleId":2}]}
4.7 項目地址
DDD Demo 代碼已經(jīng)上傳到 GitHub 中:
https://github.com/lml200701158/ddd-framewor
或者通過下面命令直接獲?。?/p>
git?clone?git@github.com:lml200701158/ddd-framework.git
5. 結語
最后,談談二哥對 DDD 的理解,我覺得 DDD 不像一門技術,我理解的技術比如高并發(fā)、緩存、消息隊列等,DDD 更像是一項軟技能,一種方法論,包含了很多設計理念。
大家不要認為,掌握了一些概念,以及 DDD 的基本思想,就掌握了 DDD,然后做項目時,照葫蘆畫瓢,這樣你會死的很慘!
只掌握 DDD 表面的東西,其實是不夠的,我覺得 DDD 最復雜的地方,其實是在它的領域設計部分,項目啟動前,你一定要設計各個領域對象,以及它們直接的交互關系。
比如我們之前做過一個項目,因為這塊沒有做好,大家一邊寫代碼,一邊還在思考,這個領域對象該如何構造,嚴重影響開發(fā)效率,最后又不得不回退到 MVC 的模式。
不要為了炫技,啥都要搞個 DDD,兩者如何選擇:
- MVC:上來就可以開干,短平快,前期用起來很香,整體開發(fā)效率也更高,所以對于緊急,或者不那么重要的項目,我會直接用 MVC 懟,不好的地方就是,后面會越來越復雜,可能最后就是一坨屎山,但是很多時候,比如老板進度催的緊,我哪想到那么多以后呢?
- DDD:前期需要花大量時間設計好領域模型,對于一些基礎組件,或者一些核心服務,如果對象模型非常復雜,建議采用 DDD,前期可能會稍微痛苦一些,但是后期維護起來會非常方便。
以上就是DDD框架落地實戰(zhàn)的詳細內容,更多關于DDD框架的資料請關注腳本之家其它相關文章!
相關文章
kafka消費者kafka-console-consumer接收不到數(shù)據(jù)的解決
這篇文章主要介紹了kafka消費者kafka-console-consumer接收不到數(shù)據(jù)的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03java實戰(zhàn)案例之用戶注冊并發(fā)送郵件激活/發(fā)送郵件驗證碼
現(xiàn)在很多的網(wǎng)站都提供有用戶注冊功能,當我們注冊成功之后就會收到封注冊網(wǎng)站的郵件,郵件里包含了我們的注冊的用戶名和密碼及激活賬戶的超鏈接等信息,這篇文章主要給大家介紹了關于java實戰(zhàn)案例之用戶注冊并發(fā)送郵件激活/發(fā)送郵件驗證碼的相關資料,需要的朋友可以參考下2021-09-09Java7和Java8中的ConcurrentHashMap原理解析
這篇文章主要介紹了Java7和Java8中的ConcurrentHashMap原理解析,對ConcurrentHashMap感興趣的讀者,一定要好好看一下2021-04-04