什么是領(lǐng)域模型
在領(lǐng)域驅(qū)動設(shè)計(Domain-Driven Design,DDD)中簿晓,領(lǐng)域?qū)ο蠓譃閷嶓w(Entity)和值對象(Value Object)。實體指的是能夠通過唯一標識符標識出來的對象谍倦,有生命周期管理越妈,而值對象僅僅表示一個值。實體的屬性是可以變的边败,只要標識符不變袱衷,它就還是那個實體捎废。但值對象的屬性卻不能變笑窜,一旦變了,它就不再是那個對象登疗,所以排截,我們會把值對象設(shè)置成一個不變的對象。在 DDD 中我們?yōu)槭裁匆獙㈩I(lǐng)域?qū)ο蠓譃閷嶓w和值對象辐益?其實主要是為了分出值對象断傲,也就是把變的對象和不變的對象區(qū)分開。
對于領(lǐng)域?qū)ο笾钦纳芷诠芾戆ǎ?/p>
- 使用工廠(Factory)模式來創(chuàng)建和銷毀領(lǐng)域?qū)ο螅?/li>
- 使用聚合(Aggregate)模式來封裝領(lǐng)域?qū)ο螅?/li>
- 使用倉儲(Repository)來查找和持久化領(lǐng)域?qū)ο蟆?/li>
工廠和倉儲理解起來一點都不難认罩,我們重點看一下聚合。
聚合就是多個實體或值對象的組合续捂,它們共同構(gòu)成了一個業(yè)務(wù)邊界垦垂。聚合里可以包含很多個對象宦搬,每個對象里還可以繼續(xù)包含其它的對象,就像一棵大樹一層層展開劫拗。但重點是间校,這是一棵樹,所以页慷,它只能有一個樹根憔足,這個根就是聚合根(Aggregate Root)。聚合根必須是一個實體酒繁,是從外部訪問這個聚合的起點滓彰。可見州袒,最簡單的聚合僅包含一個實體找蜜。
有了聚合模式后,我們所說的領(lǐng)域?qū)ο蟠蠖鄶?shù)情況下特指的是聚合稳析,也有時指的是聚合內(nèi)部的實體或值對象洗做,這個可以通過所在的上下文來判斷。
領(lǐng)域模型是關(guān)于統(tǒng)一語言的軟件模型彰居,存在于限界上下文(Bounded Context诚纸,BC)這個顯式的邊界之內(nèi),是 DDD 戰(zhàn)術(shù)設(shè)計的目標陈惰。領(lǐng)域模型通過領(lǐng)域建模得到畦徘。領(lǐng)域建模簡單來說就是識別領(lǐng)域?qū)ο螅I(lǐng)域?qū)ο笾g的關(guān)系抬闯,以及領(lǐng)域?qū)ο蟮年P(guān)鍵屬性井辆。
領(lǐng)域模型是 DDD 的核心,主要作用有兩個:
- 將領(lǐng)域知識可視化溶握,準確杯缺、深刻的反映領(lǐng)域知識,并且在業(yè)務(wù)和技術(shù)人員之間達成一致睡榆;
- 指導(dǎo)系統(tǒng)的設(shè)計和編碼萍肆。
團隊成員不管是頭腦里想的、交流中用的胀屿,還是文檔中寫的塘揣、UML圖中畫的、代碼里表達的都是對領(lǐng)域模型的直接映射宿崭,所以我們說領(lǐng)域模型是團隊所有角色在腦海里對業(yè)務(wù)知識構(gòu)建的一致畫面亲铡。
如何畫領(lǐng)域模型
領(lǐng)域模型用領(lǐng)域模型圖來呈現(xiàn),通常用UML類圖和包圖來畫。
領(lǐng)域模型的表達包括領(lǐng)域?qū)ο箨P(guān)系的表達和領(lǐng)域?qū)ο蟮谋磉_奖蔓,其中領(lǐng)域?qū)ο蟮谋磉_又包括實體的表達琅摩、值對象的表達和聚合的表達。
領(lǐng)域?qū)ο箨P(guān)系的表達
領(lǐng)域?qū)ο蟮年P(guān)系主要有兩種:關(guān)聯(lián)和泛化锭硼。
靶子和靶標是兩個實體房资,你問我:“它們之間是否有關(guān)系?”檀头,我回答說:“靶標是靶子的答案轰异,肯定是有關(guān)系的∈钍迹”于是你在它們兩個之間畫了一條線搭独,表示它們之間有關(guān)系。
你又問我:“一個靶子最多可以有幾個靶標廊镜?”我回答說:“一個靶子只能有一個靶標”牙肝。于是你在靶標那一端寫了一個“1”來表示。你接著反問:“一個靶標最多可以屬于幾個靶子嗤朴?”我回答說:“一個靶標最多屬于一個靶子配椭。”于是你在另一邊也寫上“1”雹姊。
我們可以說股缸,靶子和靶標具有一對一的關(guān)系。這里的兩個 “1” 吱雏,在 UML 中稱為多重性(multiplicity)敦姻。那么,這種關(guān)系整體上呢歧杏,在 UML 的術(shù)語里叫做“關(guān)聯(lián)”(association)镰惦。后面我們都用這種嚴格的說法,說成一對一關(guān)聯(lián)犬绒。
同理就有一對多關(guān)聯(lián)和多對多關(guān)聯(lián)旺入,其中多用 “*”來表達。
一個語言規(guī)范可以對應(yīng)多個靶子懂更,一個靶子只能歸屬一個規(guī)范:
一個靶場可以包含多個靶子眨业,一個靶子可以屬于多個靶場:
解釋完關(guān)聯(lián)的含義后,我們再來看泛化的概念:如果 A 類和 B 類可以統(tǒng)稱為 C 類的話沮协,C 類和 A、B 兩個類就具有泛化關(guān)系卓嫂,其中 C 是父類慷暂,A 和 B 是子類。泛化關(guān)系用一個空心箭頭表示,由子類指向父類行瑞。
除了“統(tǒng)稱”以外奸腺,泛化關(guān)系轉(zhuǎn)換成自然語言,還可以有另外三種說法血久,我們以教練為例進行說明:
- 對于教練來說突照,可以分成兩類:一類是管理教練,另一類是技術(shù)教練氧吐。也就是說讹蘑,泛化表示的是一種“分類”關(guān)系。
- 管理教練是教練筑舅,技術(shù)教練也是教練座慰。也就是所謂“是一個”(is-a)的關(guān)系。
-
管理教練和技術(shù)教練具有共性翠拣,那就是教導(dǎo)和實操能力版仔,我們把這個共性的概念提取出來,稱為“教練”误墓。另一方面蛮粮,管理教練和技術(shù)教練又具有“個性”,也就是兩者有差別谜慌。
image.png
“統(tǒng)稱”蝉揍、“分類”、“是一個”以及“共性 / 個性”這四種說法畦娄,雖然從表面上看不同又沾,背后的含義卻是完全一樣的。在領(lǐng)域模型里熙卡,不論哪種說法杖刷,都可以用泛化來表達〔蛋總的來說滑燃,泛化是一種強大的抽象機制,能夠同時表現(xiàn)出不同對象間的共性和個性颓鲜。
領(lǐng)域?qū)ο蟮谋磉_
領(lǐng)域?qū)ο蠓譃閷嶓w和值對象表窘,其中值對象用類圖來表達,通過<<value>>衍型(stereotype)來標識甜滨。比如時間段是一個值對象乐严,它的類圖如下所示:
需不需要用<<entity>>衍型來標識實體?這樣做當然也沒有錯衣摩,但一般來說必有性不大昂验,因為對于領(lǐng)域?qū)ο螅^值對象都是實體。
聚合使用包圖來表達既琴,內(nèi)部有一個實體為聚合根占婉,通過<<aggregate root>>衍型來標識。聚合是對一組實體和值對象的封裝甫恩,表示整體和部分的關(guān)系逆济,可以使用空心菱形(原書中 Eric Evans 用錯了,故將錯就錯)表示磺箕,也可以使用實心菱形(更符合UML奖慌,但命名有混淆)表示,但團隊內(nèi)需保持一致滞磺。筆者更傾向使用空心菱形來表示整體部分關(guān)系升薯,后續(xù)的例子都采用這種方式。整體部分關(guān)系是關(guān)聯(lián)關(guān)系的一種特例击困,原來聚合這一端的 “1” 被刪掉了涎劈,因為對于這種整體部分關(guān)系而言,這一端必然是 “1”阅茶,出于簡潔的原因蛛枚,所以就可以不寫了。
員工是一個聚合脸哀,其中一個員工實體作為聚合根代表整體蹦浦,另外兩個實體技能和工作經(jīng)驗作為整體的部分,與員工關(guān)聯(lián)撞蜂,一個員工可以有多種技能和多段工作經(jīng)驗盲镶,如下圖所示:
使用drawIO畫領(lǐng)域模型圖
假設(shè)我們已經(jīng)完成了靶場管理上下文的領(lǐng)域建模,成果如下:
- 共有 3 個聚合蝌诡,包括靶子溉贿、規(guī)范和靶場;
- 聚合根靶子聚合了值對象源文件和值對象靶標浦旱,其中靶標又由值對象靶標項組成宇色;
- 聚合根規(guī)范聚合了實體版本規(guī)范,版本規(guī)范由值對象語言規(guī)范組成颁湖,同時語言規(guī)范又由值對象語言規(guī)范項組成宣蠕;
- 聚合根靶場可以泛化為定標靶場、練習靶場和比賽靶場甥捺;
- 聚合根靶子與聚合根規(guī)范是多對一關(guān)聯(lián)抢蚀,聚合根靶子和聚合根靶場是多對多關(guān)聯(lián)。
我們使用 drawIO 來畫靶場管理上下文的領(lǐng)域模型涎永,如下圖所示:
使用 plantUML 畫領(lǐng)域模型圖
在 AI 2.0 時代思币,使用 drawIO 畫的領(lǐng)域模型圖不太方便作為業(yè)務(wù)上下文與大語言模型(Large Language Model鹿响,LLM)交流羡微,于是我們考慮使用 plantUML 來重畫領(lǐng)域模型圖谷饿。
plantUML 使用簡單的描述性語言來定義圖表,這使得用戶能夠通過編寫文本來生成圖形表示妈倔,而無需使用復(fù)雜的圖形編輯工具博投。
我們使用 plantUML 來描述靶場管理上下文的領(lǐng)域模型,如下所示:
@startuml
hide methods
hide circle
package "靶子" {
class 靶子 <<aggregate root>> {
工作空間
版本
語言
是否共享
靶標模式
狀態(tài)
可見用戶組
}
class 源文件 <<value>>{
}
class 靶標 <<value>>{
}
class 靶標項 <<value>>{
文件名
起始行號
結(jié)束行號
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
靶子 o-- "*" 源文件
靶子 o-- "*" 靶標
靶標 “1” -- "*" 靶標項
}
package "靶場" {
class 靶場 <<aggregate root>> {
語言
組織
成績
記錄
}
class 定標靶場 {
靶標負責人
靶標專家組
}
class 練習靶場 {
靶標脫敏時間
}
class 比賽靶場 {
開始時間
結(jié)束時間
靶標脫敏時間
}
靶場 <|-- 定標靶場
靶場 <|-- 練習靶場
靶場 <|-- 比賽靶場
}
package "規(guī)范" {
class 規(guī)范 <<aggregate root>> {
工作空間
已啟用版本列表
}
class 版本規(guī)范 {
版本
}
class 語言規(guī)范 <<value>>{
語言
}
class 語言規(guī)范項 <<value>>{
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
規(guī)范 o-- "*" 版本規(guī)范
版本規(guī)范 “1” -- "*" 語言規(guī)范
語言規(guī)范 “1” -- "*" 語言規(guī)范項
}
靶子.靶子 "*" -left- "*" 靶場.靶場
靶子.靶子 "*" -right- “1” 規(guī)范.規(guī)范
@enduml
在 VSCode 中使用 plantUML 插件生成領(lǐng)域模型圖如下所示:
說明:在有的系統(tǒng)中盯蝴,使用 plantUML 表達聚合根的關(guān)聯(lián)關(guān)系時毅哗,聚合根的格式必須為類名,而不是本文中的包名.類名捧挺,否則生成的領(lǐng)域模型圖將與上圖不同楼熄。
LLM 輔助畫領(lǐng)域模型圖
既然已經(jīng)可以使用 plantUML 畫領(lǐng)域模型圖了子漩,我們考慮后續(xù)降低畫其他領(lǐng)域模型圖的成本:沉淀畫領(lǐng)域模型圖的 Prompt 模版,注入目標領(lǐng)域模型邏輯,讓 LLM 生成 plantUML 文本描述亚享,然后在 VSCode 中使用 plantUML 插件生成領(lǐng)域模型圖。
我們直接給出畫領(lǐng)域模型圖的 Prompt 模版眨层,如下所示:
# 目標領(lǐng)域模型邏輯
%question%
# 輸出要求
- 使用 plantUML 文本描述锁右;
- 使用類圖和包圖來表達領(lǐng)域模型;
- 屬性僅保留中文描述很魂;
- 一對多關(guān)聯(lián)用 plantUML 語法表達就是在線的一端寫 “1”另一端寫"*" 扎酷,多對一關(guān)聯(lián)就是在線的一端寫 “*”另一端寫"1" ,多對多關(guān)聯(lián)就是在線的兩端都寫 “*” 遏匆;
- 當表達聚合根之間的關(guān)聯(lián)關(guān)系時法挨,聚合根格式必須為**包名.類名**,比如對于聚合根靶子來說幅聘,類名和包名均為靶子凡纳,則描述的格式為**靶子.靶子** ;
- 排版緊湊整齊喊暖。
# 示例
```plantuml
@startuml
hide methods
hide circle
package "靶子" {
class 靶子 <<aggregate root>> {
工作空間
版本
語言
是否共享
靶標模式
狀態(tài)
可見用戶組
}
class 源文件 <<value>>{
}
class 靶標 <<value>>{
}
class 靶標項 <<value>>{
文件名
起始行號
結(jié)束行號
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
靶子 o-- "*" 源文件
靶子 o-- "*" 靶標
靶標 “1” -- "*" 靶標項
}
package "靶場" {
class 靶場 <<aggregate root>> {
語言
組織
成績
記錄
}
class 定標靶場 {
靶標負責人
靶標專家組
}
class 練習靶場 {
靶標脫敏時間
}
class 比賽靶場 {
開始時間
結(jié)束時間
靶標脫敏時間
}
靶場 <|-- 定標靶場
靶場 <|-- 練習靶場
靶場 <|-- 比賽靶場
}
package "規(guī)范" {
class 規(guī)范 <<aggregate root>> {
工作空間
已啟用版本列表
}
class 版本規(guī)范 {
版本
}
class 語言規(guī)范 <<value>>{
語言
}
class 語言規(guī)范項 <<value>>{
缺陷編碼
缺陷大類
缺陷小類
缺陷細項
}
規(guī)范 o-- "*" 版本規(guī)范
版本規(guī)范 “1” -- "*" 語言規(guī)范
語言規(guī)范 “1” -- "*" 語言規(guī)范項
}
靶子.靶子 "*" -left- "*" 靶場.靶場
靶子.靶子 "*" -right- “1” 規(guī)范.規(guī)范
@enduml
# 任務(wù)描述
假如你是一名 DDD 專家惫企,請參考示例,根據(jù)領(lǐng)域模型邏輯來畫領(lǐng)域模型圖陵叽。
兩點說明:
- Prompt 模版中的
%question%
變量就是待用戶注入的目標領(lǐng)域模型邏輯狞尔; - Prompt 模版中的輸出要求可根據(jù)需要靈活擴充。
假設(shè)我們已經(jīng)完成了日常評審上下文的領(lǐng)域建模巩掺,其領(lǐng)域模型邏輯如下所示:
- 共有 4 個聚合偏序,包括評審組、缺陷擴展胖替、工程配置和評審研儒;
- 聚合根評審組有 2 個關(guān)鍵屬性豫缨,即名稱和成員列表,沒有聚合其他實體和值對象端朵;
- 聚合根缺陷擴展有 2 個關(guān)鍵屬性好芭,即名稱和自定義標簽, 沒有聚合其他實體和值對象冲呢;
- 聚合根工程配置有 3 個關(guān)鍵屬性舍败,即路徑、工作空間和規(guī)范版本敬拓;
- 聚合根評審有 1 個關(guān)鍵屬性邻薯,即評審人員,同時泛化了個人評審和集體評審兩個子類乘凸,并且聚合了一個值對象工程(有 2 個關(guān)鍵屬性厕诡,即具體路徑和CommitId),聚合根評審與值對象工程是一對一關(guān)聯(lián);
- 聚合根工程配置與聚合根評審組是多對一關(guān)聯(lián)营勤,聚合根工程配置與聚合根缺陷擴展是多對一關(guān)聯(lián)灵嫌,聚合根工程配置與聚合根評審是一對多關(guān)聯(lián)。
當我們要畫日常評審上下文的領(lǐng)域模型圖時冀偶,僅需將該上下文的領(lǐng)域模型邏輯注入到 Prompt 模版中的變量 %question%
即可醒第,這個過程叫模版實例化。
我們將實例化后的 Prompt 模版發(fā)送給 LLM(比如 ChatGPT):
LLM 生成的 plantUML 文本格式的領(lǐng)域模型圖如下所示:
@startuml
hide methods
hide circle
package "評審組" {
class 評審組 <<aggregate root>> {
名稱
成員列表
}
}
package "缺陷擴展" {
class 缺陷擴展 <<aggregate root>> {
名稱
自定義標簽
}
}
package "工程配置" {
class 工程配置 <<aggregate root>> {
路徑
工作空間
規(guī)范版本
}
}
package "評審" {
class 評審 <<aggregate root>> {
評審人員
}
class 個人評審 {
}
class 集體評審 {
}
class 工程 <<value>> {
具體路徑
CommitId
}
評審 <|-- 個人評審
評審 <|-- 集體評審
評審 o-- "1" 工程
}
' 關(guān)聯(lián)關(guān)系
工程配置.工程配置 "1" -down- "*" 評審.評審
工程配置.工程配置 "*" -left- "1" 評審組.評審組
工程配置.工程配置 "*" -right- "1" 缺陷擴展.缺陷擴展
@enduml
在 VSCode 中使用 plantUML 插件生成目標領(lǐng)域模型如下圖所示:
小結(jié)
領(lǐng)域模型是 DDD 的核心进鸠,修改模型就是修改代碼稠曼,修改代碼就是修改模型。軟件研發(fā)的核心難度在于處理隱藏在業(yè)務(wù)知識中的復(fù)雜度客年,那么模型就是對這種復(fù)雜度的簡化與精煉霞幅。
本文詳細闡述了領(lǐng)域模型的概念和表達方法,同時沉淀了一個畫領(lǐng)域模型的 Prompt 模版量瓜。當給 LLM 注入目標領(lǐng)域模型邏輯后司恳,可以直接生成 plantUML 文本格式的目標領(lǐng)域模型圖。LLM 輔助畫領(lǐng)域模型圖的實踐绍傲,不僅降低了我們畫領(lǐng)域模型圖的成本(節(jié)省了時間)扔傅,而且提高了我們向 LLM 注入業(yè)務(wù)知識的效率(LLM 容易理解 plantUML 文本格式的領(lǐng)域模型圖),希望對讀者有一定的收益烫饼!
參考資料
- 極客時間專欄猎塞,《手把手教你落地 DDD》,鐘敬