本文是以最簡單的方式去敘述DDD的概念。DDD 與 微服務 是緊密相關的, 所以必須是先了解 DDD 的方式才比較好的去實現 微服務。
1. 面對的問題 -- 為什么需要 DDD
- 復雜的業務不停的變化, 各模塊的耦合度不停的增大
- 經常出現業務的分歧
- 代碼的編寫方式, 不夠好(都是貧血模型)
- 不夠清晰的業務邊界
- 可擴展性差
- 無法快速的上線和功能的更新
2. 基本概念
1. 領域, 子域和界限上下文
- 一句話, 通用語言和限界上下文 是對領域驅動設計的關鍵
- 基礎概念
- 領域模型: 特定業務領域的軟件模型。
栗子: 培訓機構管理系統, 就是一個自己獨有的領域, 這個是大的領域; 1對N 中, 他有就有自己獨有的領域。
同時領域又是一個 單一的、內聚的、全功能的模型。
- 通用語言 和 限界上下文: 通用語言 就是在 限界上下文中。 通用語言 是在這個限界上下文中的共享語言; 限界上下文 是業務的概念性的邊界。
栗子1: 給學生分配學管師 張三, 在學生的限界上下文中, 分配某個學管師, 不是 USER; 張三給一對一課程考勤, 在一對一的限界上下文中, 張三 是考勤人, 不是User。 這時, 同一個 張三, 在 學生的團隊里面交流的時候, 他就是一個學管師; 在 一對一團隊中, 他就是 考勤人。 所以 通用語言是針對不同的限界上下文的, 一一對應的。
栗子2: 在 老師分配可教科目上下文 中, 老師只要是角色和姓名的屬性,老師是本上下文的一個通用語言; 如果在登錄上下文中, 那么他的登錄的密碼和賬戶、組織架構就是必要的屬性, 登錄principal是本上下文的一個通用語言。 但是 大家都是映射到 實體 User, 在不同的上下文中有不同的概念。
**一個好的限界上下文中, 每個術語都應該是一個領域的概念。** 限界上下文 主要是一個 **語義**上的邊界。
- 子域/核心域: 就是限界上下文中的最核心的領域。 比如, 訂單上下文, 訂單和訂單產品就是核心域。
- 支撐子域 : 從不同的角度看, 不同域對應不同的限界上下文, 都有可能是 核心 或者 支撐, 只是角度的不同。
栗子: 一對一上下文中, 一對一課程是一個核心子域, 而考勤操作人員 是一個 支撐的資源; 但是從用戶上下文, 人員就是一個核心域。
- 通用語言 和 限界上下文: 通用語言 就是在 限界上下文中。 通用語言 是在這個限界上下文中的共享語言; 限界上下文 是業務的概念性的邊界。
栗子1: 給學生分配學管師 張三, 在學生的限界上下文中, 分配某個學管師, 不是 USER; 張三給一對一課程考勤, 在一對一的限界上下文中, 張三 是考勤人, 不是User。 這時, 同一個 張三, 在 學生的團隊里面交流的時候, 他就是一個學管師; 在 一對一團隊中, 他就是 考勤人。 所以 通用語言是針對不同的限界上下文的, 一一對應的。
栗子2: 在 老師分配可教科目上下文 中, 老師只要是角色和姓名的屬性,老師是本上下文的一個通用語言; 如果在登錄上下文中, 那么他的登錄的密碼和賬戶、組織架構就是必要的屬性, 登錄principal是本上下文的一個通用語言。 但是 大家都是映射到 實體 User, 在不同的上下文中有不同的概念。
**一個好的限界上下文中, 每個術語都應該是一個領域的概念。** 限界上下文 主要是一個 **語義**上的邊界。
- 限界上下文 包括了 表, 領域, UI, 應用服務等, SOA, REST, 等多種對象。
- 限界上下文, 不大也不小, 足夠描述本領域的意義即可。 有可能影響的是: 技術, 任務分配, 對業務部夠理解。
- 如何實現 限界上下文和領域的劃分
抓住 通用語言 和 限界上下文
-
具體步驟:
- 確定好 物理模型 和 概念性的模型, 并且確定名字和行為。 列出認為屬于這個模塊內的所有用例。栗子, 一對一考勤, 一對一排課, 一對一扣費等。
- 創建簡單的 術語表。 從用例的關鍵字的出現頻率中, 發現通用的術語。栗子, 一對一課程, 考勤操作人, 扣費操作人, 學生等。
- 確定好 哪些是 核心域, 哪些是 支撐域。
- 多溝通和 交流, 以代碼為主, 逐漸拋棄文檔。 栗子, 表述清晰的代碼方法, 就是一個通用語言的描述。
- 做好測試后, 可以讓業務人員都一起來看 業務是否寫得正確。
- 在Idea 中, 限界上下文 表現出 module的方式
栗子: 只有學管師可以對一對一的課程進行扣費, 下面的代碼是領域服務的代碼 - 錯誤示例:
void charge(Course course, User user) {
if(!user.hasRole(StudyManager))
throw new Excption(不是學管師);
courseAttandance.setCourse(course);
courseAttandance.setStudyManager(user);
}
問題:
權限的控制不是 一對一上下文中的通用語言
一對一上下文 不應該有 用戶上下文的實體 存在(user)
核心域 應該是 course 和 courseAttandance, 支撐域應該是 考勤人員(不是學管師)
關鍵點
“開” 和 “閉” 的結合, 閉 是每個上下文內部是閉合的; 開 是上下文之間是開發的。
領域模型設計, 有 戰略 和 戰術 層面的理解。 劃分好限界上下文, 是一個戰略的體現; 而每個上下文中 使用到的技術, 是 戰術 上的不同。
戰略上, 一起是以業務相關。 不是以 技術和架構相關。
如果上下文劃分清晰,DDD 是非常輕量級的
2. 上下文映射圖
不同的限界上下文的 上下游的關系。 例如權限山下文, 和 訂單的上下文的之間的關系。
3. 架構
4. 實體
有一個 ID 的, 可以流轉在整個系統中的, 有唯一標示的對象, 且是 有數據、有業務邏輯的對象。
栗子: 就好像人, 有身份證ID, 有眼有手, 同時又是可以跑 又是可以跳。
- 原項目中的實體 - 錯誤示范
@Entity
customer{
id
getter -- name age phone address
setter
}
}
這個是我們項目中經常碰到實體, 這種實體沒有任何的業務意義, 都是在操作數據的 getter 和 setter, 而且都是一個非常強大的實體, 任何的改動, 只要一有變化, 就可以直接到數據庫中。
- DDD 應該有的實體
@Entity
customer{
id
getter -- name age phone address
setter
changePhone(int);
changeName(string);
}
}
但是在 DDD 中, 實體的概念是不同的。 是有屬性 有動作的, 是有狀態的對象。而且 與之相關聯的動作都是業務相關的。 比如, changePhone, changeName 等。 有狀態 就是意味著, 所有的對對象的修改, 都是必須 先獲取實體, 同時 執行業務的方法。
方法的名字 必須是在描述業務的行為
- 改變思考方式
與之前的一拿到需求就趕緊設計表不同, DDD 的思考更加是在業務層面上的思考。 以前是數據驅動開發的, 需要轉成 領域驅動開發的。
4. 值對象
可以整體替換的, 描述一個值的對象。 int 是一個值對象, String 都是一個值對象, 就是一個可以整體替換的對象。
整體替換 就是值對象內屬性是有關聯性。
栗子: course 中的 startTime, endTime, courseHour, length(時長), 他們是互相的影響, 改了其中的一個值, 就會對另外幾個值進行修改。
Old: 在course實體中, 設置好 courseHours 值后, 計算好另外3個, 再設置另外3個值, 這才不會導致數據出現差異。 如果某人設置好一個值后, 忘記給另外3個設置, 就會導致到數據有差異。
New: course 有一個 值對象 courseTimeObject 屬性, 包含了上述的4個屬性。 同時, 這個值對象有不同的業務方法, 比如 CourseTimeObject setHour(int 3 ), setHour中 已經計算好其他3個值了, 返回 一個 courseTimeObject 的值對象, 之后, 整體的進行替換 course.setCourseTimeObejct(courseTimeObject )
栗子: 我們的 contractProduct 有好多的屬性, 比如說是 實際剩余資金, 實際消耗資金, 優惠剩余資金, 優惠消耗資金, etc 課時。 如果同樣適用 值對象的方式, 而且這個值對象有 扣費、回滾等, 并且可以返回一個值對象, 就不會像直接設值給CP 那么容易出錯了。
值對象 可以包含業務邏輯, 但是必須是整體替換的。 同時 值對象 對于測試(直接上UnitTest就可以) 是非常的方便的。 同時出錯的范圍都給收小了。
5. 領域服務
有時候, 實體服務未必可以表現足夠的意義, 需要多個實體一起提供服務, 這時就是需要領域服務了。其實他是和實體的服務是平等的關系的, 都是完成業務邏輯的。
栗子: 權限驗證上下文中, 如果對一個用戶的 組織架構 和 用戶角色進行一并的驗證。 這里就涉及到兩個領域了, 一個是 組織架構, 一個角色。 他們都是數據權限驗證上下文的, 所以應該創建一個 領域的服務, 來做這個業務。
領域服務 有下面的特點:
- 不會管理事務
- 不能暴露內部的邏輯到外部
- 領域模型 處理的是單一的業務, 領域服務處理的是 同一個上下文中的 綜合性的業務。
6. 領域事件
領域事件是可以使用系統異構的一個方式。
栗子: 一對一扣費后, 需要對CP 和 StudentMvc 進行重新的計算。
Old: 直接調用 accountChargeRecord.save, cp.setCunsmeAmount, cpService.updateAmount, studentMvn.set...
New:當扣費完成后, 發送一個 CourseCharged 事件, 其他的上下文監聽著這個事件, 同時可以對這個事件進行處理。
領域事件的處理 有 restful 和 MQ 的方式, 各有利弊, 需要慎重考慮。
同時, 這個事件中心, 是一個基礎的架構, 而且有可能是 多重來回的消息傳遞(比如, A領域將某個值設置成pending, 發送一個消息給到B 領域, 等待B領域處理好后, 發送一個消息給到A領域, 將某值設置會 success), 方法冪等 等考慮。
7. 業務
業務 是 DDD 中最核心的點。 以前我們是根據數據來驅動開發的, 一上來就是設計表, 設計關聯等, 來一個service dao 就可以開干了; 但是 在 DDD 中, 需要對業務進行深入的理解, 分析限界上下文, 才可以下手。
實體領域模型, 值對象, 領域服務, 領域事件, 這個4個, 是對 業務邏輯的 最核心的點。 外部可以包著 任何的應用服務都可以。 比如外部可以是 微信的, APP 的, PC的應用服務 都沒有關系。 同時, 上述的實體的領域模型 和 領域服務 是 面向內部 或者 面向應用服務的業務方法, 對每個方法就是業務的方法, 其實可以設置相應的 Security 權限 Annotation, 就可以和 spring 結合得很好了。例如: Role - 對用戶進行改名Auth。
同時, 客戶化時: 經常會有, 當xxx 的時候, 如果xxx的時候, 就有xxx 的結果。 如果以業務去理解, 其實是不同的業務工作過程中的插入的處理, 所以客戶化時, 不應從數據層面去理解條件, 應從業務的角度。
8. 聚合
一堆的實體和值對象, 組成一個聚合。 聚合使用聚合根對外進行暴露。
Old: cp 有一個屬性是contract, contract 有cp list 的屬性; student 有 studentComment 的屬性, studentComment 有student的屬性。 上面是現實之中的情況, 不停的設置屬性, 不停的使用 hibernate 進行導航。
缺點: 1. 互相關聯, 是一個大的聚合, 互相耦合; 2. 無法確定修改的范圍, 因為有導航 就是可以跨域聚合去修改別的聚合的模型。
聚合, 就是在同一個限界上下文中的 不同 實體模型的的聚合。
判斷的標準:
- 在 單個事務 中管理一致性; 一個事務, 只會修改到一個聚合
- 是否 共同生死。
栗子: 比如 student, studentComment, student 沒有 comment 一樣可存活, 所以 student 與 comment 不是共存亡的聚合。 所以 student 和 comment 是兩個不同的聚合根。
有聚合后, 操作的方法的改變:
- 聚合根 是 進入這個聚合的入口, 限制入口的范圍, 同時不需要暴露聚合內部的結構。
- 一個事務, 只會修改到一個聚合。
- 可以使用 關聯 ID, 而不是導航的 實體模型。 因為 id 是值對象, 不可變的, 那么就不會有延時加載等各種問題的出現。
9. 工廠
生產聚合的一個工廠。 因為一個實體初始化, 可能需要后續多個有關聯的實體進行實體化的。 所以盡量不要使用new, 應該使用build 的方式 進行 實體的產生, 因為有一些內在的邏輯需要添加到聚合中, 比如 institutionId。
10. 資源庫
資源庫有兩種, 一種是面向集合的, 一種是面向持久化的。 使用hibernate, 就是使用面向集合的方式做資源庫。
DAO是一個面向數據角度去看資源庫的設計; DDD 資源庫, 是面向 集合Set 的角度去看。 所以類似于Set 的操作, 如 remove, add, 等 都是應用在DDD 資源庫上的。 同時, update 的操作, 都是ORM 感知到有 dirty 后 自動到數據庫中的; add 的操作, 就有需要 save 的操作。 不用在 DDD 資源庫 有特別的 update的操作。
CQRS: 最好的實現, 命令和查詢分開的方式。一個命令資源庫 是 只有 findById的查詢 操作, 和 一些必要的數據庫操作, 比如save 等; 另外一個查詢庫是可以對相應的實體進行 查詢的操作。 那么查詢庫可以方便的做成 任意的方式了, 使用springData 或者 使用其他的讀取方式都可以。 命令的資源庫方法 返回 void; 查詢的資源庫方法 返回值對象。
11. 集成界限上下文
RestFul 的集成: 1. 增加一個 Adapter(遠程連接到另外的上下文), Translator(對JSON 數據進行解析, 或者本地上下文的模型)
消息的集成: 異步的方式, 通常會造一個假的本地模型, 加上 狀態, 發送消息后, 接著監聽回傳的消息, 對這個狀態和值 進行設置。 消息中心的基礎設置會更加多。
應用服務: 負責 用例流 的協調。所有的邏輯都在領域模型中, 所以 應用服務 是一個非常薄的一層。
應用服務 管理著 事務。
應用服務的參數, 可以組裝成 命令 的結構, 用于回滾操作, 或者隊列的操作。
應用服務層, 可以做到端口和適配器模式。 類似于 MQ 發送消息的方式, 因為這樣就可以配置不同的適配器和端口, 就可以得到輸出了, 不是方法的返回。