通過一套合理的代碼結構、架構和限制,來降低 DDD 的實踐門檻,提升代碼品質、可測試性、安全性、健壯性。
廢話少說,直接上最終架構圖:
項目架構
DDD的架構能夠有效的解決傳統架構中的問題:
- 高可維護性:當外部依賴變更時,内部代碼隻用變更跟外部對接的子產品,其他業務邏輯不變。
- 高可擴充性:做新功能時,絕大部分的代碼都能複用,僅需要增加核心業務邏輯即可。
- 高可測試性:每個拆分出來的子產品都符合單一性原則,絕大部分不依賴架構,可以快速的單元測試,做到100%覆寫。
- 代碼結構清晰:通過POM module可以解決子產品間的依賴關系, 所有外接子產品都可以單獨獨立成Jar包被複用。當團隊形成規範後,可以快速的定位到相關代碼。
DP
就好像 Integer、String 是所有程式設計語言的Primitive一樣,在 DDD 裡, DP 可以說是一切模型、方法、架構的基礎
優勢:
Domain Primitive 是一個在特定領域裡,擁有精準定義的、可自我驗證的、擁有行為的 Value Object
- DP是一個傳統意義上的Value Object,擁有Immutable(不變)的特性
- DP是一個完整的概念整體,擁有精準定義
- DP使用業務域中的原生語言
- DP可以是業務域的最小組成部分、也可以建構複雜組合
使用三原則
- 讓隐性的概念顯性化
- 讓隐性的上下文顯性化
- 封裝多對象行為
Domain Primitive 和 DDD 裡 Value Object 的差別
什麼情況下應該用 Domain Primitive
- 有格式限制的 String:比如Name,PhoneNumber,OrderNumber,ZipCode,Address等
- 有限制的Integer:比如OrderId(>0),Percentage(0-100%),Quantity(>=0)等
- 可枚舉的 int :比如 Status(一般不用Enum因為反序列化問題)
-
Double 或 BigDecimal:一般用到的 Double 或 BigDecimal 都是有業務含義的,比如
Temperature、Money、Amount、ExchangeRate、Rating 等
-
複雜的資料結構:比如 Map<String, List<Integer>> 等,盡量能把 Map 的所有操作包裝掉,僅暴露必要行為
解決問題:1.接口清晰度;2.資料驗證和錯誤處理;3.業務代碼清晰度(?);4.可測試性
在真實的項目中,以前散落在各個服務或工具類裡面的代碼,可以都抽出來放在 DP 裡,成為 DP
自己的行為或屬性。這裡面的原則是:所有抽離出來的方法要做到無狀态
DO/DAO-DTO-Entity
-
Data Object
(DO、資料對象):實際上是我們在日常工作中最常見的資料模型。但是在DDD的規範裡,DO應該僅僅作為資料庫實體表格的映射,不能參與到業務邏輯中。為了簡單明了,DO的字段類型和名稱應該和資料庫實體表格的字段類型和名稱一一對應。
- Entity(實體對象):實體對象是我們正常業務應該用的業務模型,它的字段和方法應該和業務語言保持一緻,和持久化方式無關。也就是說,Entity和DO很可能有着完全不一樣的字段命名和字段類型,甚至嵌套關系。Entity的生命周期應該僅存在于記憶體中,不需要可序列化和可持久化。
- DTO(傳輸對象):主要作為Application層的入參和出參,比如CQRS裡的Command、Query、Event,以及Request、Response等都屬于DTO的範疇。DTO的價值在于适配不同的業務場景的入參和出參,避免讓業務對象變成一個萬能大對象。 對象間需要通過轉化器(Converter/Mapper)來互相轉化。而這三種對象在代碼中所在的位置也不一樣,簡單總結如下:
從使用複雜度角度來看,區分了DO、Entity、DTO帶來了代碼量的膨脹(從1個變成了3+2+N個)。但是在實際複雜業務場景下,通過功能來區分模型帶來的價值是功能性的單一和可測試、可預期,最終反而是邏輯複雜性的降低。
Assembler
實作DTO與領域對象之間的互相轉換,資料互動,幾乎總是與DTO一起出現(有一些系統使用反射機制自動實作DTO與領域對象之間的互相轉換,Appache的Commons BeanUtils就提供了類似的功能,使用Assembler進行對象資料交換更為安全與可控,并且接受編譯期檢查,但是代碼量明顯偏多。
使用反射機制自動進行象資料交換雖然代碼量很少,但大量的反射調用,性能比較差,記憶體占用多,不适合特别高并發的應用場景,一旦對象屬性名發生了變化,資料互動就會失敗,并且很難追蹤發現。總體來說,Assembler更為直白和穩妥。推薦MapStruct通過注解,在編譯時靜态生成映射代碼,其最終編譯出來的代碼和手寫的代碼在性能上完全一緻)
Repository倉儲(解決貧血模型)
-
接口名稱不應該使用底層實作的文法:我們常見的insert、select、update、delete都屬于SQL文法,使用這幾個詞相當于和DB底層實作做了綁定。相反,我們應該把
Repository 當成一個中性的類 似Collection 的接口,使用文法如
find、save、remove。在這裡特别需要指出的是區分 insert/add 和 update
本身也是一種和底層強綁定的邏輯,一些儲存如緩存實際上不存在insert和update的差異,在這個 case 裡,使用中性的 save
接口,然後在具體實作上根據情況調用 DAO 的 insert 或 update 接口。
-
出參入參不應該使用底層資料格式:需要記得的是 Repository 操作的是 Entity 對象(實際上應該是Aggregate
Root),而不應該直接操作底層的 DO。
-
應該避免所謂的“通用”Repository模式:很多 ORM
架構都提供一個“通用”的Repository接口,然後架構通過注解自動實作接口,比較典型的例子是Spring Data、Entity
Framework等,這種架構的好處是在簡單場景下很容易通過配置實作,但是壞處是基本上無擴充的可能性(比如加定制緩存邏輯),在未來有可能還是會被推翻重做。當然,這裡避免通用不代表不能有基礎接口和通用的幫助類。
體類和其業務邏輯可以随意更改,每次修改你唯一需要做的就是變更一下Converter,已經和底層實作完全解耦。
Service
在領域驅動設計的架構裡,Service的組織粒度和接口設計依據與傳統Transaction Script風格的Service是一緻的,但是兩者的實作卻有着質的差別。(Transaction Script風格的Service是實作業務邏輯的主要場所,是以往往非常厚重。而在領域驅動設計的架構裡,所有的Service隻負責協調并委派業務邏輯給領域對象進行處理,其本身并非真正實作業務邏輯,絕大部分的業務邏輯都由領域對象(Repository)承載和實作了。
Facade
為遠端用戶端提供粗粒度的調用接口,他的作用就是将一個使用者請求委派給一個或多個service進行處理,實踐Facade的過程中最難把握的問題就是Facade的粒度問題:傳統的Service均以實體為機關進行組織,而Façade應該具有更粗粒度的組織依據,較為合适的粒度依據有:一個高度内聚的子產品一個Façade或者是一個“聚合”(特指領域驅動設計中的聚合)一個Façade.
對于façade而言,99%的情況是,它隻是把某個Service的某個方法再包裹一下而已,如果把領域對象和DTO的互轉換工作移至service中進行,那麼façade将徹底變成空殼(非必須),Service的接口是面向用例設計的,是控制事務、安全的适宜場所。如果Façade的某一方法需要調用兩個以上的Service方法,需要注意事務問題。
防腐層(ACL)
Anti-Corruption Layer(防腐層或ACL)。很多時候我們的系統會去依賴其他的系統,而被依賴的系統可能包含不合理的資料結構、API、協定或技術實作,如果對外部系統強依賴,會導緻我們的系統被”腐蝕“。這個時候,通過在系統間加入一個防腐層,能夠有效的隔離外部依賴和内部邏輯,無論外部如何變更,内部代碼可以盡可能的保持不變。
ACL 不僅僅隻是多了一層調用,在實際開發中ACL能夠提供更多強大的功能:
- 擴充卡:很多時候外部依賴的資料、接口和協定并不符合内部規範,通過擴充卡模式,可以将資料轉化邏輯封裝到ACL内部,降低對業務代碼的侵入。在這個案例裡,我們通過封裝了ExchangeRate和Currency對象,轉化了對方的入參和出參,讓入參出參更符合我們的标準。
- 緩存:對于頻繁調用且資料變更不頻繁的外部依賴,通過在ACL裡嵌入緩存邏輯,能夠有效的降低對于外部依賴的請求壓力。同時,很多時候緩存邏輯是寫在業務代碼裡的,通過将緩存邏輯嵌入ACL,能夠降低業務代碼的複雜度。
- 兜底:如果外部依賴的穩定性較差,一個能夠有效提升我們系統穩定性的政策是通過ACL起到兜底的作用,比如當外部依賴出問題後,傳回最近一次成功的緩存或業務兜底資料。這種兜底邏輯一般都比較複雜,如果散落在核心業務代碼中會很難維護,通過集中在ACL中,更加容易被測試和修改。
- 易于測試:類似于之前的Repository,ACL的接口類能夠很容易的實作Mock或Stub,以便于單元測試。
- 功能開關:有些時候我們希望能在某些場景下開放或關閉某個接口的功能,或者讓某個接口傳回一個特定的值,我們可以在ACL配置功能開關來實作,而不會對真實業務代碼造成影響。同時,使用功能開關也能讓我們容易的實作Monkey測試,而不需要真正實體性的關閉外部依賴。
在一些理論架構裡ACL Facade也被叫做Gateway,含義是一樣的。
Factories工廠
聚合及聚合根(Aggregate,Aggregate Root)
- 聚合是通過定義領域對象之間清晰的所屬關系以及邊界來實作領域模型的内聚,以此來避免形成錯綜複雜的、難以維護的對象關系網。聚合定義了一組具有内聚關系的相關領域對象的集合,我們可以把聚合看作是一個修改資料的單元。
- 聚合根屬于實體對象,它是領域對象中一個高度内聚的核心對象。(聚合根具有全局的唯一辨別,而實體隻有在聚合内部有唯一的本地辨別,值對象沒有唯一辨別,不存在這個值對象或那個值對象的說法)
- 若一個聚合僅有一個實體,那這個實體就是聚合根;但要有多個實體,我們就要思考聚合内哪個對象有獨立存在的意義且可以和外部領域直接進行互動。
設計規範
-
基于領域對象 +
領域服務的DDD架構:同時要考慮到實體類的内聚和保證不變性(Invariants),也要考慮跨對象規則代碼的歸屬,甚至要考慮到具體領域服務的調用方式,了解成本比較高。
- 大多數DDD架構的核心都是實體類,實體類包含了一個領域裡的狀态、以及對狀态的直接操作。Entity最重要的設計原則是保證明體的不變性(Invariants),也就是說要確定無論外部怎麼操作,一個實體内部的屬性都不能出現互相沖突,狀态不一緻的情況。是以幾個設計原則如下: