Domain Primitive(DP)
DP概念
DP 是 DDD 中的一個基礎概念,是 DDD 中可以執行的一個最小單元,最直接的體現是,將業務相關的參數定義在一個特定的領域中(比如一個 class 文件),封裝成一個具有精准定義,自我驗證,擁有行為的 ValueObject。
行為指相關業務代碼
Value Object
區別於 Entity,擁有 id,是一個表的實例,而 VO 沒有 id,更多的強調數據,不需要對應任何表,只是一個數據的集合,一個值對象,它的一個最大特點是 Immutable(不可變性),這個值對象自從被創建出來后不會被改變,所以說這個對象中的屬性最好都是被 private final 修飾。
DP原則
- 將隱性的概念顯性化
例:電話號是用戶的一個屬性,屬於隱形概念,但實際上獲取電話號的地區號(行為)才是真正的業務邏輯,因此需要將電話號的概念顯性化,在“電話號”對象里實現該行為。 - 將隱性的上下文顯性化
例:當我們做支付功能時,實際上需要的一個入參對象是支付金額 + 貨幣類型,因此可以把這兩個概念組合成一個獨立的完整概念:Money - 封裝多對象行為
例:如果一段業務邏輯涉及到多個對象,就需要用 DP 包裝起來
什么情況可以考慮使用 DP 進行業務優化?
1 接口清晰度,通過接口參數是否能夠顯示業務
2 數據校驗和錯誤處理是否統一維護,數據校驗的依賴性,比如地區號的獲取依賴於電話號,那地區號校驗就要放在“電話號”類中做校驗
3 業務邏輯代碼的清晰度,膠水代碼是否存在於核心業務代碼中,區分核心業務代碼和非核心業務代碼(膠水代碼:從一些入參里抽取一部分數據,然后調用一個外部依賴獲取更多的數據,然后通常從新的數據中再抽取部分數據用作其他的作用。)
DDD應用架構
View Object(VO)-- 視圖對象,代表展示層需要顯示的數據,通常由 DTO 轉換后展示。
Data Transfer Object(DTO)-- 數據傳輸對象,主要作為 Application 層的入參和出參。
Entity - 基於領域邏輯的實體類,擁有 ID,它的字段和數據庫儲存不需要有必然的聯系,不僅包含數據,還有行為,字段也不僅僅是 String 等基礎類型,而應該盡可能用 Domain Primitive 代替,可以避免大量的校驗代碼。
Data Object(DO)- DO 是單純的和數據庫表的映射關系,每個字段對應數據庫表的一個 column,DO 只有數據,沒有行為。
Repository - 只負責定義 Entity 對象的存儲和讀取方法,返回對象一般為 Entity。
RepositoryImpl - 實現數據庫存儲讀取的細節,通過加入 Repository 接口,底層的數據庫連接可以根據實際情況用不同的實現類來替換。
Mapper(DAO) - DAO 對應的是一個特定的數據庫類型的操作,CRUD。
Builder/Factory - 實現 DO 與 Entity 之間的轉化。
Anti-Corruption Layer(ACL)- 防腐層,很多時候我們的系統會去依賴其他的系統,而被依賴的系統可能包含不合理的數據結構、API、協議或技術實現,如果對外部系統強依賴,會導致我們的系統被”腐蝕“。這個時候,通過在系統間加入一個防腐層,能夠有效的隔離外部依賴和內部邏輯,無論外部如何變更,內部代碼可以盡可能的保持不變。
ACL作用
- 適配器:很多時候外部依賴的數據、接口和協議並不符合內部規范,通過適配器模式,可以將數據轉化邏輯封裝到 ACL 內部,降低對業務代碼的侵入。比如轉化對方的入參和出參,序列化反序列化等,讓入參出參更符合我們的標准。
- 緩存:對於頻繁調用且數據變更不頻繁的外部依賴,通過在 ACL 里嵌入緩存邏輯,能夠有效的降低對於外部依賴的請求壓力。同時,很多時候緩存邏輯是寫在業務代碼里的,通過將緩存邏輯嵌入 ACL ,能夠降低業務代碼的復雜度。
- 兜底:如果外部依賴的穩定性較差,一個能夠有效提升我們系統穩定性的策略是通過 ACL 起到兜底的作用,比如當外部依賴出問題后,返回最近一次成功的緩存或業務兜底數據。這種兜底邏輯一般都比較復雜,如果散落在核心業務代碼中會很難維護,通過集中在 ACL 中,更加容易被測試和修改。
- 功能開關:有些時候我們希望能在某些場景下開放或關閉某個接口的功能,或者讓某個接口返回一個特定的值,我們可以在 ACL 配置功能開關來實現,而不會對真實業務代碼造成影響。
ACL 原理
- 對於依賴的外部對象,我們抽取出所需要的字段,生成一個內部所需的 VO 或 DTO 類
- 構建一個新的 Facade(GateWay),在 Facade 中封裝調用鏈路,將外部類轉化為內部類
- 針對外部系統調用,同樣的用 Facade 方法封裝外部調用鏈路
Domain Layer(領域層):Entity、Domain Primitive 和 Domain Service 都屬於領域層,領域層沒有任何外部依賴關系。Domain Primitive 是無狀態的。當某個行為影響到多個 Entity 時,屬於跨實體的業務邏輯,在這種情況下就需要通過一個第三方的領域服務(Domain Service)來完成。
Application Layer(應用層):Application Service、Repository、ACL 類屬於應用層,應用層依賴領域層,但不依賴具體實現。
Infrastructure Layer(基礎設施層):ACL,Repository 等的具體實現類,通常依賴外部具體的技術實現和框架。
Domain-Driven Design(DDD):領域驅動設計,架構思路是先寫 Domain 層的業務邏輯,然后再寫 Application 層的組件編排,最后才寫每個外部依賴的具體實現。
解耦實現
DTO Assembler:將 1 個或多個相關聯的 Entity 轉化為 1 個或多個 DTO。
Data Converter:Entity 到 DO 的轉化器。
轉換一般使用 MapStruct 庫
Repository:當成一個中性的類,使用語法如 find、save、remove,使用中性的 save 接口,然后在具體實現上根據情況調用 DAO 的 insert 或 update 接口,根據 Aggregate 的 ID 是否存在且大於 0 來判斷一個 Aggregate 是需要更新還是插入。這樣做是為了概念上和數據庫解綁,不去直接使用 insert、select、update、delete。
Repository 復雜實現:一次操作中,並不是所有 Aggregate 里的 Entity 都需要變更,但是如果用簡單的寫法,會導致大量的無用 DB 操作。具體實現參照:Repository復雜實現
領域層設計規范
Entity
通過聚合根保證主子實體的一致性
在稍微復雜一點的領域里,通常主實體會包含子實體,這時候主實體就需要起到聚合根的作用,即:
- 子實體不能單獨存在,只能通過聚合根的方法獲取到。任何外部的對象都不能直接保留子實體的引用。
- 子實體沒有獨立的 Repository,不可以單獨保存和取出,必須要通過聚合根的 Repository 實例化。
- 子實體可以單獨修改自身狀態,但是多個子實體之間的狀態一致性需要聚合根來保障。
不可以強依賴其他聚合根實體或領域服務
一個實體類不能直接在內部直接依賴一個外部的實體或服務。正確的對外部依賴的方法有兩種:
- 只保存外部實體的 ID:強烈建議使用強類型的 ID 對象,而不是 Long 型 ID。強類型的 ID 對象不單單能自我包含驗證代碼,保證 ID 值的正確性,同時還能確保各種入參不會因為參數順序變化而出 bug。
- 針對於“無副作用”的外部依賴,通過方法入參的方式傳入。如果方法對外部依賴有副作用,不能通過方法入參的方式,只能通過 Domain Service 解決。
任何實體的行為只能直接影響到本實體(和其子實體),不能直接修改其他的實體類。
分層理解
Interface層
1 網絡協議的轉化:通常這個已經由各種框架給封裝掉了,我們需要構建的類要么是被注解的 bean,要么是繼承了某個接口的 bean。
2 統一鑒權:比如在一些需要 AppKey+Secret 的場景,需要針對某個租戶做鑒權的,包括一些加密串的校驗
3 Session 管理:一般在面向用戶的接口或者有登陸態的,通過 Session 或者 RPC 上下文可以拿到當前調用的用戶,以便傳遞給下游服務。
4 限流配置:對接口做限流避免大流量打到下游服務
5 前置緩存:針對變更不是很頻繁的只讀場景,可以前置結果緩存到接口層
6 異常處理:通常在接口層要避免將異常直接暴露給調用端,所以需要在接口層做統一的異常捕獲,轉化為調用端可以理解的數據格式
7 日志:在接口層打調用日志,用來做統計和 debug 等。一般微服務框架可能都直接包含了這些功能。
規范:
- Interface 層的 HTTP 和 RPC 接口,返回值為 Result,捕捉所有異常。
- 一個 Interface 層的類應該是“小而美”的,應該是面向“一個單一的業務”或“一類同樣需求的業務”,需要盡量避免用同一個類承接不同類型業務的需求。
Application層
1 ApplicationService 應用服務:最核心的類,負責業務流程的編排,但本身不負責任何業務邏輯。
2 DTO Assembler:負責將內部領域模型轉化為可對外的 DTO。
3 Command、Query、Event 對象:作為 ApplicationService 的入參。
4 返回的DTO:作為 ApplicationService 的出參。
Command、Query、Event對象
CQE vs DTO
CQE:CQE 對象是 ApplicationService 的輸入,是有明確的“意圖”的,所以這個對象必須保證其“正確性”。
DTO:DTO 對象只是數據容器,只是為了和外部交互,所以本身不包含任何邏輯,只是貧血對象。
規范
- Application 層的所有接口返回值為 DTO,不負責處理異常,可以直接拋異常,不用統一處理。
- ApplicationService 的接口入參只能是一個 Command、Query 或 Event 對象,CQE 對象需要能代表當前方法的語意。唯一可以的例外是根據單一ID查詢的情況,可以省略掉一個 Query 對象的創建。
- CQE 對象的校驗應該前置,避免在 ApplicationService 里做參數的校驗。可以通過 JSR303/380 和 Spring Validation 來實現。
- 針對於不同語意的指令,要避免 CQE 對象的復用,比如常見的場景是“Create 創建”和“Update 更新”應該分開。
Application Service 是業務流程的封裝,不處理業務邏輯
判斷是否業務流程的幾個點:
1 不要有 if/else 分支邏輯:通常有分支邏輯的,都代表一些業務判斷,應該將邏輯封裝到 Domain Service 或者 Entity 里,但這不代表完全不能有 if 邏輯,比如:
ItemDO item = itemService.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
但這里僅僅代表了中斷條件,具體的業務邏輯處理並沒有受影響。
2 不要有任何計算邏輯。
3 數據的轉化交給其他對象來做,數據的轉化可以交給其他對象來做。
COLA4.0架構圖
1 適配層(Adapter Layer):負責對前端展示的路由和適配,對於傳統B/S系統而言,adapter 就相當於 MVC 中的 controller,包括 vo、和assembler 類似的轉換類(實現 vo 與 dto 之間轉換);
2 應用層(Application Layer):主要負責獲取輸入,組裝上下文,參數校驗,調用領域層做業務處理,如果需要的話,發送消息通知等。層次是開放的,應用層也可以繞過領域層,直接訪問基礎實施層(利用 mapper 訪問),包括 executorImpl、assembler(實現 dto 與 model 之間轉換或 dto 與 po 之間轉換)、dto、rpc(實現 Client 中的 facade);
3 領域層(Domain Layer):主要是封裝了核心業務邏輯,並通過領域服務(Domain Service)和領域對象(Domain Entity)的方法對 App 層提供業務實體和業務邏輯計算。領域是應用的核心,不依賴任何其他層次,包括abilityImpl;
4 基礎實施層(Infrastructure Layer):主要負責技術細節問題的處理,比如數據庫的 CRUD、搜索引擎、文件系統、分布式服務的 RPC 等。此外,領域防腐的重任也落在這里,外部依賴需要通過 gateway 的轉義處理,才能被上面的Domain層使用,包括po、converter(實現 model 與 po 之間轉換);
5 Client(封裝成 SDK 供外部調用):api(facade)、dto;
各個包結構的簡要功能描述