什么是領域模型
在領域驅動設計(Domain-Driven Design,DDD)中,領域對象分為實體(Entity)和值對象(Value Object)。實體指的是能夠通過唯一標識符標識出來的對象,有生命周期管理,而值對象僅僅表示一個值。實體的屬性是可以變的,只要標識符不變,它就還是那個實體。但值對象的屬性卻不能變,一旦變了,它就不再是那個對象,所以,我們會把值對象設置成一個不變的對象。在 DDD 中我們為什么要將領域對象分為實體和值對象?其實主要是為了分出值對象,也就是把變的對象和不變的對象區分開。
對于領域對象,它的生命周期管理包括:
- 使用工廠(Factory)模式來創建和銷毀領域對象;
- 使用聚合(Aggregate)模式來封裝領域對象;
- 使用倉儲(Repository)來查找和持久化領域對象。
工廠和倉儲理解起來一點都不難,我們重點看一下聚合。
聚合就是多個實體或值對象的組合,它們共同構成了一個業務邊界。聚合里可以包含很多個對象,每個對象里還可以繼續包含其它的對象,就像一棵大樹一層層展開。但重點是,這是一棵樹,所以,它只能有一個樹根,這個根就是聚合根(Aggregate Root)。聚合根必須是一個實體,是從外部訪問這個聚合的起點。可見,最簡單的聚合僅包含一個實體。
有了聚合模式后,我們所說的領域對象大多數情況下特指的是聚合,也有時指的是聚合內部的實體或值對象,這個可以通過所在的上下文來判斷。
領域模型是關于統一語言的軟件模型,存在于限界上下文(Bounded Context,BC)這個顯式的邊界之內,是 DDD 戰術設計的目標。領域模型通過領域建模得到。領域建模簡單來說就是識別領域對象,領域對象之間的關系,以及領域對象的關鍵屬性。
領域模型是 DDD 的核心,主要作用有兩個:
- 將領域知識可視化,準確、深刻的反映領域知識,并且在業務和技術人員之間達成一致;
- 指導系統的設計和編碼。
團隊成員不管是頭腦里想的、交流中用的,還是文檔中寫的、UML圖中畫的、代碼里表達的都是對領域模型的直接映射,所以我們說領域模型是團隊所有角色在腦海里對業務知識構建的一致畫面。
如何畫領域模型
領域模型用領域模型圖來呈現,通常用UML類圖和包圖來畫。
領域模型的表達包括領域對象關系的表達和領域對象的表達,其中領域對象的表達又包括實體的表達、值對象的表達和聚合的表達。
領域對象關系的表達
領域對象的關系主要有兩種:關聯和泛化。
靶子和靶標是兩個實體,你問我:“它們之間是否有關系?”,我回答說:“靶標是靶子的答案,肯定是有關系的。”于是你在它們兩個之間畫了一條線,表示它們之間有關系。
你又問我:“一個靶子最多可以有幾個靶標?”我回答說:“一個靶子只能有一個靶標”。于是你在靶標那一端寫了一個“1”來表示。你接著反問:“一個靶標最多可以屬于幾個靶子?”我回答說:“一個靶標最多屬于一個靶子。”于是你在另一邊也寫上“1”。
我們可以說,靶子和靶標具有一對一的關系。這里的兩個 “1” ,在 UML 中稱為多重性(multiplicity)。那么,這種關系整體上呢,在 UML 的術語里叫做“關聯”(association)。后面我們都用這種嚴格的說法,說成一對一關聯。
同理就有一對多關聯和多對多關聯,其中多用 “*”來表達。
一個語言規范可以對應多個靶子,一個靶子只能歸屬一個規范:
一個靶場可以包含多個靶子,一個靶子可以屬于多個靶場:
解釋完關聯的含義后,我們再來看泛化的概念:如果 A 類和 B 類可以統稱為 C 類的話,C 類和 A、B 兩個類就具有泛化關系,其中 C 是父類,A 和 B 是子類。泛化關系用一個空心箭頭表示,由子類指向父類。
除了“統稱”以外,泛化關系轉換成自然語言,還可以有另外三種說法,我們以教練為例進行說明:
- 對于教練來說,可以分成兩類:一類是管理教練,另一類是技術教練。也就是說,泛化表示的是一種“分類”關系。
- 管理教練是教練,技術教練也是教練。也就是所謂“是一個”(is-a)的關系。
-
管理教練和技術教練具有共性,那就是教導和實操能力,我們把這個共性的概念提取出來,稱為“教練”。另一方面,管理教練和技術教練又具有“個性”,也就是兩者有差別。
image.png
“統稱”、“分類”、“是一個”以及“共性 / 個性”這四種說法,雖然從表面上看不同,背后的含義卻是完全一樣的。在領域模型里,不論哪種說法,都可以用泛化來表達。總的來說,泛化是一種強大的抽象機制,能夠同時表現出不同對象間的共性和個性。
領域對象的表達
領域對象分為實體和值對象,其中值對象用類圖來表達,通過<<value>>衍型(stereotype)來標識。比如時間段是一個值對象,它的類圖如下所示:
需不需要用<<entity>>衍型來標識實體?這樣做當然也沒有錯,但一般來說必有性不大,因為對于領域對象,除過值對象都是實體。
聚合使用包圖來表達,內部有一個實體為聚合根,通過<<aggregate root>>衍型來標識。聚合是對一組實體和值對象的封裝,表示整體和部分的關系,可以使用空心菱形(原書中 Eric Evans 用錯了,故將錯就錯)表示,也可以使用實心菱形(更符合UML,但命名有混淆)表示,但團隊內需保持一致。筆者更傾向使用空心菱形來表示整體部分關系,后續的例子都采用這種方式。整體部分關系是關聯關系的一種特例,原來聚合這一端的 “1” 被刪掉了,因為對于這種整體部分關系而言,這一端必然是 “1”,出于簡潔的原因,所以就可以不寫了。
員工是一個聚合,其中一個員工實體作為聚合根代表整體,另外兩個實體技能和工作經驗作為整體的部分,與員工關聯,一個員工可以有多種技能和多段工作經驗,如下圖所示:
使用drawIO畫領域模型圖
假設我們已經完成了靶場管理上下文的領域建模,成果如下:
- 共有 3 個聚合,包括靶子、規范和靶場;
- 聚合根靶子聚合了值對象源文件和值對象靶標,其中靶標又由值對象靶標項組成;
- 聚合根規范聚合了實體版本規范,版本規范由值對象語言規范組成,同時語言規范又由值對象語言規范項組成;
- 聚合根靶場可以泛化為定標靶場、練習靶場和比賽靶場;
- 聚合根靶子與聚合根規范是多對一關聯,聚合根靶子和聚合根靶場是多對多關聯。
我們使用 drawIO 來畫靶場管理上下文的領域模型,如下圖所示:
使用 plantUML 畫領域模型圖
在 AI 2.0 時代,使用 drawIO 畫的領域模型圖不太方便作為業務上下文與大語言模型(Large Language Model,LLM)交流,于是我們考慮使用 plantUML 來重畫領域模型圖。
plantUML 使用簡單的描述性語言來定義圖表,這使得用戶能夠通過編寫文本來生成圖形表示,而無需使用復雜的圖形編輯工具。
我們使用 plantUML 來描述靶場管理上下文的領域模型,如下所示:
@startuml
hide methods
hide circle
package "靶子" {
class 靶子 <<aggregate root>> {
工作空間
版本
語言
是否共享
靶標模式
狀態
可見用戶組
}
class 源文件 <<value>>{
}
class 靶標 <<value>>{
}
class 靶標項 <<value>>{
文件名
起始行號
結束行號
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
靶子 o-- "*" 源文件
靶子 o-- "*" 靶標
靶標 “1” -- "*" 靶標項
}
package "靶場" {
class 靶場 <<aggregate root>> {
語言
組織
成績
記錄
}
class 定標靶場 {
靶標負責人
靶標專家組
}
class 練習靶場 {
靶標脫敏時間
}
class 比賽靶場 {
開始時間
結束時間
靶標脫敏時間
}
靶場 <|-- 定標靶場
靶場 <|-- 練習靶場
靶場 <|-- 比賽靶場
}
package "規范" {
class 規范 <<aggregate root>> {
工作空間
已啟用版本列表
}
class 版本規范 {
版本
}
class 語言規范 <<value>>{
語言
}
class 語言規范項 <<value>>{
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
規范 o-- "*" 版本規范
版本規范 “1” -- "*" 語言規范
語言規范 “1” -- "*" 語言規范項
}
靶子.靶子 "*" -left- "*" 靶場.靶場
靶子.靶子 "*" -right- “1” 規范.規范
@enduml
在 VSCode 中使用 plantUML 插件生成領域模型圖如下所示:
說明:在有的系統中,使用 plantUML 表達聚合根的關聯關系時,聚合根的格式必須為類名,而不是本文中的包名.類名,否則生成的領域模型圖將與上圖不同。
LLM 輔助畫領域模型圖
既然已經可以使用 plantUML 畫領域模型圖了,我們考慮后續降低畫其他領域模型圖的成本:沉淀畫領域模型圖的 Prompt 模版,注入目標領域模型邏輯,讓 LLM 生成 plantUML 文本描述,然后在 VSCode 中使用 plantUML 插件生成領域模型圖。
我們直接給出畫領域模型圖的 Prompt 模版,如下所示:
# 目標領域模型邏輯
%question%
# 輸出要求
- 使用 plantUML 文本描述;
- 使用類圖和包圖來表達領域模型;
- 屬性僅保留中文描述;
- 一對多關聯用 plantUML 語法表達就是在線的一端寫 “1”另一端寫"*" ,多對一關聯就是在線的一端寫 “*”另一端寫"1" ,多對多關聯就是在線的兩端都寫 “*” ;
- 當表達聚合根之間的關聯關系時,聚合根格式必須為**包名.類名**,比如對于聚合根靶子來說,類名和包名均為靶子,則描述的格式為**靶子.靶子** ;
- 排版緊湊整齊。
# 示例
```plantuml
@startuml
hide methods
hide circle
package "靶子" {
class 靶子 <<aggregate root>> {
工作空間
版本
語言
是否共享
靶標模式
狀態
可見用戶組
}
class 源文件 <<value>>{
}
class 靶標 <<value>>{
}
class 靶標項 <<value>>{
文件名
起始行號
結束行號
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
靶子 o-- "*" 源文件
靶子 o-- "*" 靶標
靶標 “1” -- "*" 靶標項
}
package "靶場" {
class 靶場 <<aggregate root>> {
語言
組織
成績
記錄
}
class 定標靶場 {
靶標負責人
靶標專家組
}
class 練習靶場 {
靶標脫敏時間
}
class 比賽靶場 {
開始時間
結束時間
靶標脫敏時間
}
靶場 <|-- 定標靶場
靶場 <|-- 練習靶場
靶場 <|-- 比賽靶場
}
package "規范" {
class 規范 <<aggregate root>> {
工作空間
已啟用版本列表
}
class 版本規范 {
版本
}
class 語言規范 <<value>>{
語言
}
class 語言規范項 <<value>>{
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
規范 o-- "*" 版本規范
版本規范 “1” -- "*" 語言規范
語言規范 “1” -- "*" 語言規范項
}
靶子.靶子 "*" -left- "*" 靶場.靶場
靶子.靶子 "*" -right- “1” 規范.規范
@enduml
# 任務描述
假如你是一名 DDD 專家,請參考示例,根據領域模型邏輯來畫領域模型圖。
兩點說明:
- Prompt 模版中的
%question%
變量就是待用戶注入的目標領域模型邏輯; - Prompt 模版中的輸出要求可根據需要靈活擴充。
假設我們已經完成了日常評審上下文的領域建模,其領域模型邏輯如下所示:
- 共有 4 個聚合,包括評審組、缺陷擴展、工程配置和評審;
- 聚合根評審組有 2 個關鍵屬性,即名稱和成員列表,沒有聚合其他實體和值對象;
- 聚合根缺陷擴展有 2 個關鍵屬性,即名稱和自定義標簽, 沒有聚合其他實體和值對象;
- 聚合根工程配置有 3 個關鍵屬性,即路徑、工作空間和規范版本;
- 聚合根評審有 1 個關鍵屬性,即評審人員,同時泛化了個人評審和集體評審兩個子類,并且聚合了一個值對象工程(有 2 個關鍵屬性,即具體路徑和CommitId),聚合根評審與值對象工程是一對一關聯;
- 聚合根工程配置與聚合根評審組是多對一關聯,聚合根工程配置與聚合根缺陷擴展是多對一關聯,聚合根工程配置與聚合根評審是一對多關聯。
當我們要畫日常評審上下文的領域模型圖時,僅需將該上下文的領域模型邏輯注入到 Prompt 模版中的變量 %question%
即可,這個過程叫模版實例化。
我們將實例化后的 Prompt 模版發送給 LLM(比如 ChatGPT):
LLM 生成的 plantUML 文本格式的領域模型圖如下所示:
@startuml
hide methods
hide circle
package "評審組" {
class 評審組 <<aggregate root>> {
名稱
成員列表
}
}
package "缺陷擴展" {
class 缺陷擴展 <<aggregate root>> {
名稱
自定義標簽
}
}
package "工程配置" {
class 工程配置 <<aggregate root>> {
路徑
工作空間
規范版本
}
}
package "評審" {
class 評審 <<aggregate root>> {
評審人員
}
class 個人評審 {
}
class 集體評審 {
}
class 工程 <<value>> {
具體路徑
CommitId
}
評審 <|-- 個人評審
評審 <|-- 集體評審
評審 o-- "1" 工程
}
' 關聯關系
工程配置.工程配置 "1" -down- "*" 評審.評審
工程配置.工程配置 "*" -left- "1" 評審組.評審組
工程配置.工程配置 "*" -right- "1" 缺陷擴展.缺陷擴展
@enduml
在 VSCode 中使用 plantUML 插件生成目標領域模型如下圖所示:
小結
領域模型是 DDD 的核心,修改模型就是修改代碼,修改代碼就是修改模型。軟件研發的核心難度在于處理隱藏在業務知識中的復雜度,那么模型就是對這種復雜度的簡化與精煉。
本文詳細闡述了領域模型的概念和表達方法,同時沉淀了一個畫領域模型的 Prompt 模版。當給 LLM 注入目標領域模型邏輯后,可以直接生成 plantUML 文本格式的目標領域模型圖。LLM 輔助畫領域模型圖的實踐,不僅降低了我們畫領域模型圖的成本(節省了時間),而且提高了我們向 LLM 注入業務知識的效率(LLM 容易理解 plantUML 文本格式的領域模型圖),希望對讀者有一定的收益!
參考資料
- 極客時間專欄,《手把手教你落地 DDD》,鐘敬