DDD領域驅動設計如何進行工程化落地
引言
前面幾篇文章中,筆者給大家闡述了DDD領域驅動設計的三大過程,重點圍繞如何通過戰(zhàn)略設計與戰(zhàn)術設計進行DDD領域模型分析以及沉淀,但是還沒有涉及到工程層面的落地。所有的這些架構理論或者設計模式到最后都是為了讓我們的代碼結構更加清晰,擴展性以及維護性更強。從而開發(fā)出bug少穩(wěn)定性更好的應用。因此本文重點介紹如何進行DDD工程化落地。????
DDD領域分層
當我們完成邊界上下文的劃分以及領域模型的構建之后,就需要進行微服務的工程結構設計了。在進行工程結構落地之前,我們需要先確定微服務內部的領域分層結構。首先我們要思考一個問題,為什么要進行領域分層呢?實際上領域分層就是一種分而治之的思想,主要為了避免將代碼工程開發(fā)成一坨大泥球,各種業(yè)務復雜邏輯以及技術細節(jié)都糅合在一起,導致工程后期難以維護同時也會削弱領域模型的完整性。另外通過領域分層設計,更加容易開發(fā)出高內聚低耦合的軟件服務,在模塊復用以及擴展性方面也會有更好的表現(xiàn)。
搞清楚為什么進行領域分層之后,我們來確定下如何進行微服務內部的領域分層,因為分層設計的好壞直接決定了我們微服務的工程結構合理性以及后期團隊落地的效果。不過遺憾的是,真正的領域驅動設計在怎么規(guī)范工程結構上面實際也沒有非常明確具體的規(guī)范,因此我們需要根據自己的實踐經驗以及思考和理解來進行劃分設計。下圖中左邊的分層方式是 Eric Evans在《領域驅動設計》中提出的,但是這種分層方式實際上是存在明顯不足的。為什么這么說呢?
大家都比較熟悉MVC的開發(fā)方式,因此在團隊中進行DDD落地的時候,很多同學有疑問為什么要讓基礎設施層反向依賴領域層呢,大家都覺得很別扭。按照正常邏輯來說,領域模型發(fā)生變化后需要進行持久化保存,很明顯是領域層依賴基礎設施層,但是在工程落地的時候還是基礎設施層依賴領域層,這是為什么呢?實際上無論是什么樣的架構都遵循這樣的設計原則,我們都認為業(yè)務領域是核心域,核心域對外部的依賴越少越好,因此需要實現(xiàn)將技術復雜度與業(yè)務復雜度相分離。那么在 基于DDD的架構中,領域層就是核心層因此它的對外依賴越少越好,也就是說應該是非核心依賴核心而不是核心依賴非核心。
在我們以往的開發(fā)模式中,一般都是service接口去調用dao接口進行相關的數據操作,但是我們發(fā)現(xiàn)一旦我們進行一些優(yōu)化操作,比如增加緩存來提升數據查詢的效率,我們就需要修改service層的代碼,但是實際上增加緩存屬于技術實現(xiàn)細節(jié),并不在業(yè)務范疇之內,可實際情況就是技術細節(jié)有變化就會影響到業(yè)務層,因此這樣的狀況明顯是不合理的。
因此上圖中優(yōu)化后的依賴倒置,表面上是基礎設施層依賴領域層,其本質是技術實現(xiàn)細節(jié)依賴于接口抽象,這是一種編程思想的轉變。將repo層的接口定義在domain層,具體實現(xiàn)細節(jié)由基礎實施層去完成,這樣實現(xiàn)了對于技術實現(xiàn)細節(jié)的解耦。同時不僅保證了domain層模型的穩(wěn)定性,也提升了基礎設施層實現(xiàn)的靈活性。
各層模型數據對象
在介紹各層對象之前,我們先思考一個問題。為什么每一層都要有不同的數據模型對象呢?不同分層在進行接口調用的時候,每次都要進行模型對象轉換,很多時候對象中的參數還都是一樣的,這樣做不是多此一舉嗎?這也是我在團隊中推行DDD領域驅動設計落地的時候,很多同學提出來的疑問。
但是大家有沒有想過一個問題,假設我們使用一個模型數據對象來串接代碼中的各個分層,如果哪一天數據庫表字段增加了或者修改了,那么這個變化會在各個分層中蔓延開來,這樣即使做了應用分層但是實際上和一個大泥球的應用沒有什么本質區(qū)別,另外對于核心的領域層來說也需要屏蔽底層細節(jié)變化對于領域模型的影響,避免領域模型穩(wěn)定性問題。因此為了避免上述問題的發(fā)生,各個分層應該都有數據自己的模型數據對象,各司其職。
VO(View Object,視圖對象):該層的視圖數據對象主要的作用就是將應用層的數據進行組裝后形成用于頁面展示的數據。
DTO(Data Transfer Object,數據傳輸對象):DTO主要作為Application層的入參和出參,用于用戶接口層與應用層之間的數據傳輸。比如接口參數中的Command、Query以及事件Event,以及Request、Response等都屬于DTO的范疇。DTO的價值在于適配不同的業(yè)務場景的入參和出參,避免讓業(yè)務對象變成一個萬能大對象。
Model(領域對象):領域對象是我們常說的核心的領域模型對象,它的字段和方法應該具備強烈的業(yè)務語義,和持久化方式無關。也就是說,Entity和PO很可能有著完全不一樣的字段命名和字段類型,甚至嵌套關系。Entity的生命周期應該僅存在于內存中,不需要可序列化和可持久化。
PO (Persistent Objec,持久化對象):實際上是我們在日常工作中最常見的數據模型。但是在DDD的規(guī)范里,PO應該僅僅作為數據庫物理表格的映射,不能參與到業(yè)務邏輯中。為了簡單明了,PO的字段類型和名稱應該和數據庫物理表格的字段類型和名稱一一對應,這樣我們不需要去跑到數據庫上去查一個字段的類型和名稱。
各層數據流轉
上文中分別說到了領域分層結構以及各個數據對象的不同含義和用途,那么我們接下來就看下各個數據對象在DDD的各個領域分層中是怎么進行數據流轉的吧。
在用戶接口層,它需要接收來自WEB端、APP端以及其他的外部數據請求,并將請求通過DTO向應用層進行傳遞,根據應用層返回的DTO數據,再將DTO轉化為頁面需要呈現(xiàn)的VO數據。
我們通過Query對象表示查詢,用Command對象表示數據操作。當請求到達應用層后,如果需要調用外部服務的接口,那么我們需要通過應用層的防腐層進行調用。為什么需要防腐層呢?主要就是為了隔離變化,防止外在服務的數據變化影響應用層的代碼,如果真的需要修改那么直接在防腐層中進行修改就好。
在領域層,我通常使用的是model,可以理解為業(yè)務領域模型,主要包括實體以及值對象。在應用層會將model作為參數進行領域層接口的調用完成核心的業(yè)務邏輯。在一些其他的書中,很多人喜歡使用DO來作為領域層的數據承載對象,但是我個人還是覺得model更適合,因為從名稱上面更好理解一點,更加直觀一點。
領域層中包含了倉儲的接口,具體的實現(xiàn)在基礎設施層中,這是一種依賴倒置的設計方式,實現(xiàn)領域層與基礎層的解耦。大致的數據轉化流向如下圖所示。
工程結構落地
在確定好領域分層各層的依賴關系之后,我們需要設計下具體可落地的工程結構,如下圖所示。
starter層:該層屬于用戶接口層,服務的啟動類也在該層,主要負責服務的啟動以及對外提供REST接口或者RPC接口。
business層:主要負責業(yè)務邏輯的編排,不負責具體的業(yè)務邏輯,因此該層應該是比較薄的。
integration層:ACL層,即防腐層,主要與外部服務接口進行交互,它的存在主要為了將微服務本身的業(yè)務模型與外部服務的模型進行隔離,避免外部服務模型的變化影響到自身服務領域模型的穩(wěn)定性。
domain層:領域層屬于核心層,所有的業(yè)務領域模型以及領域服務都在該層,沉淀了整個業(yè)務域中的業(yè)務領域模型,也就說核心的業(yè)務邏輯都落在此層,同時定義了repository層的接口。
common層:通用層,主要放一些支撐其他業(yè)務的代碼,比如各種工具類,各種常量定義、錯誤碼定義以及多語言等。
repository層:屬于基礎設施層,主要負責與數據庫、Redis等進行交互,實現(xiàn)領域層定義的接口。
總結
本文主要和大家聊了怎樣進行DDD領域驅動設計的落地,分析了為什么要進行領域分層以及為什么要實現(xiàn)依賴倒轉的領域分層結構,同時基于依賴倒轉的領域分層結構設計了可落地的微服務工程結構。希望通過本文可以為大家在落地DDD的時候提供一點工程結構設計的思路。后面的文章將從代碼層面入手和大家分享下如何通過代碼實現(xiàn)DDD落地。