DDD概念復雜難懂,實際落地如何設計代碼實現模型?
寫在前面:
今天我接著跟大家聊一聊,DDD概念復雜難懂,實際落地如何設計代碼實現模型。或許你是剛看到關于這部分的內容,想著這里我有必要多說一句,關于這個話題,框架上,分為這樣兩部分講的:方法篇 + 實踐篇。
前一部分,方法篇。旨在詳細介紹DDD所包含的幾個核心概念,以及圍繞這些概念所構建的DDD代碼實現模型的組成結構。至于為何有必要講,上一篇開頭我有明確告訴大家。
另外,考慮到有的朋友可能才剛點進來,還沒看過上一篇,或者沒來得及看,故而這里也再點明說一下 我想分享這一話題的必要性,以便于幫你快速知曉可以或多或少有哪方面的收獲。
開門見山說,可惜的是,目前業界關于如何實施這些概念,并沒有一套統一的標準和規范,這就導致我們在具體的開發過程中,常常感到無從下手。
為此,本文專門提煉了一整套DDD代碼實現模型。
此外,關于看的方式,我多說一句。基于是分為前后兩部分更新發布的,這就涉及到先后了。若上一篇你還未看,朋友,建議你可以先花上幾分鐘,或者結合文章中大小標題的思路引導,大致了解下行文框架。咱們可以過完上一篇,再進入這篇的分享,結合著,效果更好。
?01 如何設計DDD代碼實現模型?
在分析DDD代碼實現模型時,對于上一篇提到的四個組成部分,我們需要梳理它們的代碼結構和依賴關系。針對代碼結構,我們需要明確代碼包的組成,以及內部所包含的技術組件。
在明確了包結構之后,依賴關系指的是我們需要進一步明確這些代碼包和技術組件之間的交互關系。基于這兩點,讓我們先來討論領域對象的代碼實現模型。
?? 領域對象代碼實現模型
針對領域對象,我們通常用“domain”這個單詞,對代碼包結構的頂層包進行命名,在該包結構下的所有技術組件,都屬于領域對象的范疇。
具體而言,在DDD中,領域對象包括領域模型對象、領域事件、資源庫以及應用服務所涉及到的命令和查詢對象,其中領域模型對象可以分為聚合、實體和值對象這三大類。
因此,在DDD所有的代碼實現模型中,領域對象涉及的代碼結構最為復雜,可以分成兩個層次,如圖1所示。
圖1
可以看到,這里的“domain”代表整個領域對象,而“model”則代表領域模型對象,請注意這兩者在命名上的區別,以及它們之間的從屬關系。領域對象是DDD代碼實現模型的基礎,包含核心業務邏輯的實現。
?? 應用服務代碼實現模型
類似地,針對應用服務,我們通常使用“application”來命名頂層包結構。應用服務包含查詢服務和命令服務這兩大類,所以在子包的命名上,也會用“commandservice”和“queryservice”加以區分,如圖2所示。
圖2
顯然,命令服務和查詢服務,分別依賴于領域對象代碼實現模型中的命令對象和查詢對象,我們用虛線表示這層依賴關系。在DDD的代碼實現模型中,應用服務可以說是交互關系最為復雜的一個代碼模型。
一方面,它需要將命令和查詢操作,分派給聚合對象等領域模型對象。
另一方面,它也需要分別和基礎設施,以及其他限界上下文進行交互。
關于后者,我們在討論到案例分析時,還會做進一步展開。
?? 基礎設施代碼實現模型
其實,所謂的基礎設施,指的是DDD應用程序中所使用到的各種具體技術、工具和框架。常見的基礎設施類組件主要包括這幾個方面:
- 數據持久化(Persistence)
- 消息通信(Messaging)
- 系統配置(Config)
- 安全控制(Security)
因此,基礎設施的包結構并不是固定的,而是根據具體的技術開發要求進行靈活的組織,這里給出一個常見的包結構,如圖3所示。針對基礎設施,我們使用了“infrastructure”,對這一包結構進行命名。
圖3
上圖中有一點需要注意,代表數據持久化的“persistence”包,和代表消息通信的“messaging”包,在基礎設施代碼實現模型中是最常見的,因為它們分別對應著領域對象中的資源庫和領域事件。
在DDD中,資源庫和領域事件的定義位于領域對象代碼實現模型中,它們與具體的實現技術無關。而與具體實現技術相關的持久化和消息通信,則位于基礎設施代碼實現模型中。這里體現了領域對象與實現技術相互分離的設計原則。
?? 上下文集成代碼實現模型
最后,我們來討論上下文集成代碼實現模型。需要注意的是,這個模型實現起來難度最大,因為涉及到多種系統集成技術體系。
針對這一代碼實現模型,我們首先需要明確它是面向多個限界上下文的,所以我們需要考慮數據的流向,也就是所謂的內向(Inbound)數據和外向(Outbound)數據。
一方面,限界上下文,需要暴露訪問入口供其他上下文進行使用。站在當前上下文角度看,這是一個Inbound操作。而當某一個上下文向外部上下文發起請求時,這就是一個Outbound操作,如圖4所示。
圖4
在代碼實現模型的設計上,我們也將采用“inbound”和“outbound”來命名包結構。那么這兩個包結構下,應該包含哪些技術組件呢?
我們先來討論“outbound”包結構,如圖5所示。 圖中,“rest”包中的REST API將外部請求,轉化為內部的Command和Query對象,并交由應用服務進行處理。在這個轉化過程中,通常需要引入專門的DTO(Data Transfer Object,數據傳輸對象)對象,和組裝器(Assembler)對象。
圖5
同時,“eventpublisher”包中的事件發布器(Event Publisher),則用來面向外部限界上下文發布領域事件。
接著,我們討論“inbound”包結構。在一個限界上下文中,數據的Inbound操作主要有兩類,一類是防腐層(Anti-Corruption Layer,ACL),用來向遠程REST API發起請求并獲取結果。另一類是用來完成對領域事件進行響應的事件處理器(Event Handler),如圖6所示。
圖6
基于上下文集成過程,兩個上下文中的“inbound”和“outbound”包結構中所包含的技術組件,實際上是一一對應的,如圖7所示。
可以看到,一個限界上下文“inbound”中的“acl”和“eventhandler”,分別對應著另一個限界上下文“outbound”中的“rest”和“eventpublisher”。
圖7
至此,關于DDD中四大類代碼實現模型,已介紹完。在接下來的內容中,我們將基于一個具體的應用場景,通過案例分析,將這些代碼實現模型付諸于實踐。基于這個案例,你可以將本文前面介紹的所有內容,和日常開發過程聯系起來,進一步掌握將模型轉化為具體代碼的實現方法和技巧。
02 DDD代碼實現模型案例分析
在現實世界中,工單處理是一個非常常見的業務需求。而工單的發起,通常都是因為用戶需要對訂單進行咨詢或投訴。
在這個場景中,基于DDD的設計方法,我們可以分別拆分出工單(Ticket)、客服(Staff),以及訂單(Order)這三個限界上下文。在這三個上下文中,Ticket上下文,會分別與Staff和Order這兩個上下文進行集成,從而創建工單申請,如圖8所示。
請注意,圖中展示了Ticket上下文,所具備的兩種不同的上下文集成方式。
針對Staff上下文,Ticket上下文將使用REST API,完成對工單中客服數據的獲取。
而針對Order上下文,則使用了領域事件,即一旦Order的狀態發生變化,Order上下文會發送對應的領域事件到Ticket上下文中。
圖8
?? Ticket上下文代碼實現模型示例
顯然,針對這一場景,Ticket上下文同時具備了Inbound和Outbound操作。因此,它的代碼實現模型是最完整的,如圖9所示。
圖9
上圖中,我們使用IDEA這款開發工具和Spring Boot這一特定的開發框架,構建了Ticket限界上下文的代碼實現模型。我們可以很清晰地看到,DDD四種代碼實現模型的表現形式,就是五個頂層的代碼包結構。其中,上下文集成代碼實現模型同時包含了“inbound”和“outbound”這兩個代碼包。
我們再對這些頂層代碼包結構做展開,可以得到如圖10所示的子代碼包結構。
圖10(上下滑動查看)
上圖所示的所有子代碼包結構,在前面的內容中也都已經給出了相應的描述,這里便不再贅述。
Ticket上下文中,命令服務TicketCommandService完成了對Staff服務的上下文集成,這時候采用的是防腐層ACL組件,示例代碼如下所示。
可以看到,這里使用AclStaffService這個ACL組件,對Staff服務發起了遠程調用,然后把返回結果填充到命令對象,并創建Ticket聚合。最終,我們通過TicketRepository完成了對聚合對象的持久化操作。
圖11
上述AclStaffService,就完成了對Staff上下文所提供的REST API的調用,示例代碼如下所示。這里用到了Spring自帶的RestTemplate模板工具類,完成對遠程HTTP端點的訪問操作。
圖12
?? Staff上下文代碼實現模型示例
在Staff上下文,我們需要完成對上述REST API的構建,它的代碼工程結構如下圖所示。
可以看到,相較Ticket上下文,Staff上下文的代碼結構比較簡單,因為該上下文只需要提供對外的“outbound”包,而基礎設施部分也只需要完成對領域對象的持久化操作即可。
圖13
?? Order上下文代碼實現模型示例
最后,我們來到Order限界上下文,它的代碼實現模型是這樣的,可以一同看下。
圖14
我們知道Order上下文,提供了針對Order數據的領域事件發布機制,所以它的“outbound”包中包含了用于發布領域事件的“eventpublisher”子包,并提供了一個OrderEventPublisherService,如下所示。
圖15
這里通過Spring Cloud Stream,實現了領域事件的發布。而在Ticket上下文中,我們同樣可以基于Spring Cloud Stream,實現對該領域事件的監聽和消費,示例代碼如下所示。
圖16
請注意,上述OrderUpdatedEventHandler,位于Ticket上下文“inbound”包的”eventhandler”子包中。
關于這些具體實現代碼的講解不是本文的重點,你可以參考筆者在Github上的案例代碼進行系統學習:https://github.com/tianminzheng/customer-service。
03 總結和延伸思考
今天的分享到這里就結束了。本文內容詳細回答了開發人員,在實現DDD應用程序中所碰到的一個核心問題,即如何構建DDD的代碼實現模型。之所以要討論這個話題,原因在于DDD中的很多概念都比較晦澀難懂,而業界也沒有為如何實現這些概念,提供統一的開發規范和標準。
而通過將DDD中的各種復雜概念與具體代碼實現模型進行映射,在幫我們更好地理解這些概念的同時,也能夠將它們直接應用到日常開發過程中。
通過本文內容的介紹,開發人員可以結合自身的業務開發需求,設計一套完整的DDD代碼實現模型。這里也附上全文思維導圖,助你回顧、梳理思路等。
圖17 全文思維框架導圖-幫助你快速回顧、梳理、總結
?? 最后,我覺得還是有必要強調一點
本文中給出的DDD代碼實現模型,也只是一個參考模型。而代碼實現模型的設計,也與具體所采用的技術體系有一定關聯。在本文所展示的案例中,我們使用了Spring Boot、Spring Cloud Stream等Spring家族中的開發框架,來開發DDD應用程序。
而如果你使用Axon這種基于事件溯源模式的DDD開發框架,那么在代碼實現模型中,就需要引入用于事件分發和存儲的Gateway、EventStore等組件,而位于基礎設施中的傳統數據持久化組件,可能就不一定會被使用到。
當然,基于我們今天介紹的內容,相信你并不難對這套DDD代碼實現模型進行擴展。DDD作為一種系統建模方法論,也存在一些諸如分層架構、整潔架構、六邊形架構等多種架構風格。
針對每種架構風格,我們都需要設計對應的代碼實現模型。
而基于本文中介紹的內容,通過對DDD中各個核心概念與實現模型之間進行合理的映射,我在文中提供了一套設計代碼實現模型的系統方法,從而幫助你可以應對不同架構風格的實現要求。
這也是本文的核心價值所在。?