Domain-Driven Design 領域驅動設計 初學者指南
tags: Domain-Driven Design
category: 軟體開發
description: Domain-Driven Design (DDD) 領域驅動設計 初學者指南
created_at: 2024/10/28 12:00:00
前言
最近在學習 Domain-Driven Design (DDD)
領域驅動設計,拜讀了一些書籍和文章,所以我打算來記錄一些淺見,希望能夠幫助到一些初學者(?)讓更多人入坑。
也許我的理解有誤,如果有錯誤的地方,歡迎指正。 畢竟我也是剛接觸這個主題,還有很多東西需要學習,所以如果你看一看發現好像哪裡怪怪的,可能你是對的(?)
至於我為什麼要學習 DDD
,主要是我希望把我的碩士論文做得更好,所以希望能夠透過 DDD
來提升我的專案品質。
預備知識
學習 DDD
之前,建議先了解以下幾個概念:
- 物件導向相關的概念 (
Object-Oriented
),不管你是OO
、OOP
、OOA
、OOD
、OOAD
都可以。 - 軟體架構 (
Software Architecture
) - 軟體設計原則 (
Software Design Principles
) - 軟體設計模式 (
Software Design Patterns
)
為什麼? 因為 DDD
是建立在這些基礎之上的,也許不用到非常深入,但是至少要有基本的了解。
但也許在這一篇文章中用到的不多,畢竟只是淺談,而且也不是實作的文章,所以如果你有一些基礎知識,應該就可以了,至於先列出來是因為之後沒意外的話應該也會寫幾篇是關於實作的,但不知道是什麼時間就是了。
我很慶幸我在學習 DDD
之前,已經學到了其他相關的基礎知識,這樣我才能夠更快的理解 DDD
的概念。
預防針
我會以我所理解的方式來解釋 DDD
,所以可能會有一些地方用詞並不那麼精確,主要是希望用更淺顯易懂的方式來傳達 DDD
的概念,好讓大家的學習門檻降低。
摘錄一段 Implementing Domain-Driven Design(IDDD)
一書中的一段話: I’m not going to tell you that there isn’t a learning curve. To put it bluntly, the learning curve can be steep. Yet, this book has been put together to help flatten the curve as much as possible.
嗯... 看完後的我覺得果然還是有點陡峭呢,因此我希望能夠拉低更多學習曲線,讓大家更容易理解 DDD
,至少在入門先使用更簡單的方式來理解。
註: 所以,如果要看精確的用詞的話,建議直接看書籍或者其他資源,這篇文章主要是讓一些被這也許陡峭的學習門檻勸退(?)的人,可以更容易的理解 DDD
的概念。
目錄
DDD
的目標- 軟體的複雜度到底是怎麼來的
- 所以到底什麼是領域(
Domain
)? - 怎麼解決這些問題(
Domain
)? - 小結(?)
- 所以到底怎麼設計
- 戰略設計之
Domain
- 戰略設計之
Context Map
- 戰略設計之
Ubiquitous Language
- 戰術設計 簡介
- 實作方式
- 結語
DDD
的目標
- 讓你說的話和你的程式碼說的話一致。
- 保持高內聚,低耦合(獨立性)。
針對第一點,平常注重程式碼可讀性的人(例如我(?)),可能會覺得這是理所當然的事情,但在學習這些方法論之前,更多時候可能都是靠「經驗」,例如可能透過 Clean Code
來提升程式碼的可讀性,但 Clean Code
和 DDD
比起來,我認為 Clean Code
是 「細節」,而 DDD
是更高層次的東西。
針對第二點,平常熟悉 OO
,或者說是 SOLID
原則的人,可能又又又覺得這不是理所當然的事情嗎,所以又回到了那一句,你怎麼知道你所做的是「高內聚,低耦合」呢?於是可能又回到了「經驗」,但 DDD
可以幫你梳理出一個更清晰的「戰略設計」,也就是大方向該怎麼設計(這裡還沒到軟體架構,架構只是一種「細節」)。
也許會有天才(?),透過大量的「經驗」,其實沒有學過這套方法論(?),但真的做到了這件事,又或者是覺得說我沒學過,但這不就是我現在做的事嗎,之類的。
學過設計模式的人,可能覺得有似曾相似的感覺,設計模式也是不用特別學,當你寫的經驗夠多,對自己有要求(?),然後踩過一些痛點,自然而然也可能衍生出屬於你的設計模式(也可能包含 GoF
的設計模式)。
但我個人還是鼓勵直接從書籍或資源去學習這些「模式」,畢竟這些是前人的經驗(?),可以省下你踩坑的時間,也可以藉由自己的腦袋去衍生出更多有趣的東西。
軟體的複雜度到底是怎麼來的
雖然我平常開發就是會去問需求方是不是要我講的這個流程(對於 user
),盡可能貼近使用者要的東西,但既然在學習過程中有看到這個過程,就來稍微提一下到底軟體是怎麼樣複雜起來的。
這裡先撇開模組間的設計,就只站在更大方向來探討到底軟體複雜度是如何產生的,也就是先不論你是不是寫出一個大泥球(?),到處耦合的程式碼或是工具相關所帶來的複雜度。 (( 當然也不談時間造成的
這裡也先不談維護所導致的增加,而是從最一開始開始漸進談起。
從最一開始: 大家都知道傳話時會產生偏差,所以這個偏差,很可能從一開始就存在,因此地基不穩,後面蓋起來就變成危樓。 (中間的傳話可能很多層)
就算確定需求之後且傳達到工程師身上(假設對於需求理解無誤),有些工程師可能會過度設計(?),此時就為未來和當下多埋下了一顆雷。詳細參見 YAGNI
(You aren't gonna need it
)。
那麼,到底什麼樣的設計是好的設計呢? 也許還是回到 「經驗」,但 DDD
可以幫助你設計,也許不是最好的,但至少給了你一個方向。
所以到底什麼是領域(Domain
)?
領域(Domain
),簡單來說就是你的專案要解決的問題,也就是你的專案要做的事情。
舉例來說,如果你是做一個 Blog
系統,那麼你的領域就是 Blog
,你要解決的問題就是 Blog
相關的問題。
那麼什麼是 Blog
相關的問題呢? 這就要看你的需求是什麼,例如你的需求是要有 文章
、留言
、標籤
、分類
、斗內
等等,這些就是你的子領域。這個部落格還沒有 斗內
功能
我相信這樣的例子應該很容易理解(?)
然後這些領域們,會再透過一些方法論來進行設計,來解決這些問題。
怎麼解決這些問題(Domain
)?
在 DDD
當中,有個 Context
的概念,Context
就是一個個的解決方案(?)
為什麼說是一個個呢? 因為如果只有一個 Context
,也就是都不要拆分,整個 Domain
都讓我用一個 Context
來解決。
這樣做的話你的系統會變成一個大泥球(?),全部都綁在一起。
所以這時就有了 Bounded Context
的概念,Bounded Context
就是 Boundary
和 Context
的結合。
如果可以的話,最好是一個 Domain
透過一個 Bounded Context
來解決,但是實際情況下因為要解決的問題可能很複雜,所以可能會有多個 Bounded Context
來解決。
但不管怎麼樣,「設計」本來就沒有一定要怎麼樣,只要想好溝通好,有足夠的理由,就可以了。
小結(?)
相信看到這邊,在看看下面這張圖,就能夠輕易理解那張圖到底在講什麼了。
圖片來源 - IDDD
簡單來說這是在描述一個電商系統的 Domain
和 Bounded Context
。
有一個個的子領域,透過一個個的 Bounded Context
來解決。
所以到底怎麼設計
前面講了一大堆,但到底怎麼設計呢?
設計又分兩種,分別是:
- 戰略設計 (
Strategic Design
) - 戰術設計 (
Tactical Design
)
簡單說一個是大方向(?),一個是完成這個大方向的方法(細節)(?)。
舉例來說,剛才那一堆內容就是屬於戰略設計(Strategic Design
),但好像還缺了什麼(?),既然剛才都切了 Bounded Context
,那這些 Bounded Context
要怎麼互動、整合? 待會下面會補充,這還是屬於戰略設計的部分。
而戰術設計(Tactical Design
) 就是你要怎麼實作這些 Bounded Context
內的東西,也就是在有限的問題當中你要怎麼解決? 換句話說,在特定的問題中,你的模組、物件之類的要怎麼設計。
戰略設計之Domain
其實 Domain
也還有細分為三種 Subdomain
,分別是:
Core Domain
: 就是你的核心領域,也就是你的主要問題,這個部分你要特別注意,因為這是你的核心競爭力,你也應該投注最多資源。Supporting Domain
: 就是你的支撐(?)領域,也就是你的輔助問題,這個部分需要客製化來完成(可能只有你有,別人可能沒有這個問題)。Generic Domain
: 就是你的通用領域,也就是不管在哪個系統都會有的問題,這個部分你可以使用一些通用的解決方案。(例如: 寄信、地圖、第三方支付之類的)
從上而下剛好就是你應該投注的資源比例,Core Domain
> Supporting Domain
> Generic Domain
。
然而怎麼去區分這些 Subdomain
並沒有強制一定怎麼樣,我認為這只是告訴你應該怎麼樣分配資源。
因為這些 Subdomain
並沒有誰比較重要之分,因為任一個 Subdomain
沒有解決,代表整個問題並沒有被完全解決。
而且你的 Core Domain
在別人眼裡可能是 Generic Domain
,所以這些只是一個參考而已,舉例來說你就是做第三方支付的,那你的 Core Domain
肯定就是跟支付相關的問題,但是在別人眼裡可能就是 Generic Domain
。
戰略設計之Context Map
前面提到了 Bounded Context
,描述這些 Bounded Context
之間的關係的圖,就是 Context Map
。
而這些關係的名稱叫做 Context Mapping
,這些關係主要有以下幾種:
Partnership
Shared Kernel
Open Host Service
/Published Language
Separate Way
Customer-Supplier
Conformist
Anti-corruption Layer
接著一個一個來簡要說明:
Partnership
這兩個 Bounded Context
要嘛一起成功 要嘛一起失敗,有可能是個反模式,請慎用(?),因為兩邊綁得太緊,甚至有可能產生循環依賴(雖然也可以透過一些方式解決)。
他的圖會長得像這樣:
Shared Kernel
這兩個 Bounded Context
會共用一些東西,意思上就是很常見的交集的圖:
但在畫 Context Map
時,可能會畫成這樣,其中 U
代表 Upstream
,D
代表 Downstream
,分別代表上游和下游:
像是這樣的話代表 Context A
和 Context B
會共用一些東西,而他共用的東西在 Context C
中。
Open Host Service
/ Published Language
Open Host Service
就像是提供一個 API
(Rest
、RPC
…)
Published Language
則是定義一個溝通方式(json
、xml
、protobuf
…)
這兩個的圖會長得像這樣:
Separate Way
這兩個 Bounded Context
會走自己的路,不會互相影響:
或者是說乾脆就不整合了,自己做,通常是發現整合太複雜,覺得獲得的收益小於整合的成本,就乾脆不整合了。
比較直覺的優點是適當的重複可以獲得更多的好處(解耦),但缺點是如果無腦都自己來,那自然過度重複也自然順著違反了 DRY
(Don’t Repeat Yourself
) 原則。
但...如果用得當,反而是一個比較好的設計,因為你獲得了「高內聚,低耦合」。
Customer-Supplier
客戶與供應商的關係,上游開發的時候會去顧及下游,然後下游可以去使用上游的東西,而圖形很簡單:
Conformist
剛才提到客戶與供應商的關係,這時有一個很類似的關係,不過這時上游不管下游死活了,但下游還是想去存取上游時,這時就是 Conformist
關係。
這種情況可能也不能怪上游(?),因為可能根本不在同個團隊中,而這時,你可能有三種因應方式:
Separate Way
(自己做)- 透過接下來介紹的
ACL
(Anti-corruption Layer
) 防腐層 - 透過
Conformist
關係 (妥協)
而這時的圖會長得像這樣:
直接妥協也不見得不好,只是要去看上游所提供的 Model
做的好不好,如果做得很好,這樣不用多經過一層 ACL
的轉換,或許也是一個不錯的選擇。 當然要看你對他的信任程度,做出相應的決定
Anti-corruption Layer
防腐層,一個翻譯層,把髒髒的東西隔離一下,這樣你的 Model
就不會被污染了。
先來看看他的圖:
嗯... 看了都長一個樣,那來看看書上的範例:
圖片來源 - IDDD
可以看到他的 CollaboratorService
透過 UserRoleAdapter
去使用 HTTPClient
存取外面的資源,然後再透過一個 CollaboratorTranslator
去做一層轉換,這樣就不會污染到 CollaboratorService
的 Model
。
嗯... 如果還是覺得有點抽象,那麼接著看看下面這張圖:
雖然他的物件名稱不同,但你可以很清楚的看到他的過程,主要是 adapter
和 translator
的關係。
這個 adapter
拿到東西之後去使用 translator
去轉換之後再回傳給 service
。
戰略設計之 Ubiquitous Language
Ubiquitous Language
是一個很重要的概念,也是 DDD
的核心概念之一。
那怎麼到現在才講呢? 因為我覺得放這剛剛好,看到最後就知道了。
記得在最前面說的 DDD
的目標之一是讓你說的話和你的程式碼說的話一致,這個 Ubiquitous Language
就是要達到這個目標。
不管是誰,不管是使用者、領域專家(?)、開發者,都要使用同一個語言來溝通。
領域專家是誰:
- 就是你的
Domain
的專家,也就是你的需求方,他們知道你的系統要解決的問題。 - 或是對你的特定領域有深入了解的人。
也許是廢話,但我想應該能理解
總之總之,就是給我講一樣的話,嗯..好像有點抽象,稍微具體一點來說就是:
- 文章: 不要使用者認知是
article
,開發者認知是post
。 - 使用者: 不要使用者認知是
user
,開發者認知是member
。
上面這些誤會可能會導致開發者寫出 post
或 member
相關的程式碼,且行為也會以這些命名去做衍生,這樣就會導致程式碼和需求方的認知不一致。
雖然可能覺得沒什麼,但長期來看會導致這個 Gap
越來越大;記得一開始提到的傳話問題嗎,就類似這種感覺。
但要符合這個 Ubiquitous Language
並不是那麼容易,因為每個人的認知都不一樣,而且每個人都應該要持續學習:
- 開發者要去學這些
Domain Knowledge
。 - 領域專家要去學這些
Technical Knowledge
。
但對於領域專家來說他並不需要學習太多的 Technical Knowledge
,只要溝通上不要有障礙就好,所以開發者也要忍住,不要愛炫技講一堆有的沒的,當然也不要過早抽象,但也許基本的 Factory
、Repository
之類的大方向,如果領域專家並不排斥,反正也不是要他實作,只要溝通的話,我想應該不成問題,而且這樣會加速整體的效率。
例如也許可以說,透過 Factory
去生成 XXX
,然後這個 XXX
就可以來完成 YYY
之類的。
這樣應該不會很技術吧,不會吧不會吧。
接著當最終目標達到了,你的程式碼恰好符合需要,也不會有過度設計的問題。
然後如果實施得當,你的領域專家甚至也能讀懂你的程式碼(至少測試程式碼),然後檢查測試有沒有遺漏的部分;但如果領域專家連讀測試程式碼都非常困難很難懂,那也許就是一種改進的訊號。
最後一點是,這個 Ubiquitous Language
必須只針對特定 Context
有自己的通用語言,而不是整個 Domain
共通甚至整個公司共通,這樣注定會導致失敗。
為什麼呢? 嗯... 這有點像是你在宣告物件的時候,你可能會像下面這樣
class User {
private String name;
private String email;
private String password;
}
但是你不會這樣吧
class User {
private String userName;
private String userEmail;
private String userPassword;
}
換句話說,針對特定的 Context
就很像是第一種宣告,在 User
這個環境下,那個 name
你可以很明確知道是 User
的 name
,而不是其他的 name
,例如文章名稱、產品名稱??
所以當你把通用語言套用到多個 Context
相當於耦合,更不用說你套用到整個公司,那根本是 God Object
,一場災難。
戰術設計 簡介
戰術設計(Tactical Design
) 主要是在解決你的 Bounded Context
內的問題,也就是在解決你的子領域(Subdomain
) 的問題。
這邊可以利用一些 DDD
相關的元件(?)來做設計,例如:
Entity
Value Object
Aggregate
Repository
: 嗯... 就是一般常見的那個Repository
,相信既然都在看這篇文章,應該對這個不陌生。Factory
:同上,就是一般常見的那個Factory
Domain Service
Application Service
Domain Event
所以接著會主要介紹 Entity
、Value Object
、Aggregate
、Domain Service
、Application Service
、Domain Event
這幾個元件。
Entity
Entity
是一個有唯一識別的物件,也就是說他的 id
是唯一的,只要 id
一樣,那就是同一個 Entity
。
屬性可以改變或不變,但只要 id
一樣,那就是同一個 Entity
。
舉個簡單的例子,人也是一個 Entity
,只要你的 id
一樣,那就是同一個人,不管你的名字、年齡、身高體重怎麼改變,只要 id
一樣,那就是同一個人。
然後上面可以綁定一些行為,例如 Person
可以 eat
、sleep
、work
之類的,但這邊的命名就請遵循 Ubiquitous Language
。
Value Object
Value Object
是一個沒有唯一識別的物件,他只認屬性,也就是說他的屬性一樣,那就是同一個 Value Object
。
屬性不能被修改,因為他只當作一個 Value
來使用,所以他的屬性一樣,那就是同一個 Value Object
。
舉個簡單的例子,貨幣也是一個 Value Object
,只要你的幣別和面額都一樣,那就是同一個貨幣。
例如你不會說我錢包裡面的新台幣跟你的錢包裡面的新台幣是不一樣的,只要面額一樣我們就可以交換(?)
當然你也許會說我們的錢上面還有一些其他資訊不同,例如序號、年份等等之類的,但這些不影響他是同一個貨幣,你還是必須要所有屬性都一樣,那才是同一個 Value Object
,畢竟我想你也不會硬性規定一定要序號一樣才是相同的貨幣吧。
然後 Value Object
上面也可以綁定一些方法,但這些方法不會改變他的屬性,而是透過他的屬性去做一些事情。
不管是以貨幣來說可能是一些計算,或許你也可以針對他來做自訂的比較行為之類的。
因為我想語意上同一個 Value Object
的屬性都一樣才會是同一個 Value Object
,是指在實作上,概念上作為分辨(?),但以現實生活中,應用上,以貨幣來說,我們並不會去管他的序號、年份等等,只要面額、幣種一樣,那就是同一個貨幣。
這句話可能有點抽象,但也許可以簡單做個分類,如果你很注重唯一性,那就用 Entity
,如果你很注重屬性,那就用 Value Object
。
而屬性會不會改變,你會變的話就只有 Entity
這個選項了
Aggregate
嗯...到了具 IDDD
一書所說的 among all DDD tactical guidance, this pattern is one of the least well understood.
的 Aggregate
了。
雖然這邊提到的概念可能不會完整到足以導致 Aggregate
成為書中所提的最難理解的概念之一(?),但總之就是稍微介紹一下。
Aggregate
是由一個或多個的 Entity
和 Value Object
所組成,他們一起形成一個整體,這個整體就是 Aggregate
。
然後這些 Entity
當中會有其中一個成為 Root Entity
,這個 Root Entity
也就是所謂的 Aggregate Root
,這個 Aggregate Root
會負責整個 Aggregate
的生命週期。
既然說這個 Aggregate Root
負責整個 Aggregate
的生命週期,那他就會負責整個 Aggregate
的一些行為,例如新增、修改、刪除等等,也就是說其實他也是一個資料庫的 Transaction
的邊界。
這樣的話,你就可以保證整個 Aggregate
的一致性,也就是在任何時候,都不會存在不合法的狀態。
說到這邊,還有個很重要的,就是外面都只能透過 Aggregate Root
來存取 Aggregate
,這樣才能保證整個 Aggregate
的一致性,不然直接用裡面的東西你也管不到。除非你用很髒的方式(?)...
這邊附上一張圖:
圖片來源 - Domain-Driven Design: Tackling Complexity in the Heart of Software
可以看到說 Customer
只能透過 Aggregate Root
去存取,不能直接繞過 Car
去存取 Tire
等等元件。
這樣的話,你就可以保證整個 Aggregate
的一致性,不會有不合法的狀態。
而且同時也減少了依賴的複雜度。
Domain Service
Domain Service
是一個不屬於任何 Entity
或 Value Object
的行為,他是一個純粹的行為,不會有任何狀態,但可以有副作用。
什麼意思? 沒狀態又有副作用?
- 沒狀態: 他不會有任何狀態,相同的輸入會有相同的輸出。
- 有副作用: 他可能會去改變一些狀態,但這些狀態不會是他的狀態。
其實你會發現這兩項沒有衝突,可以同時存在。
雖然說可以有副作用,但不代表你一定要讓他有副作用,所以還是謹慎設計。
所以基本上就是放不是和放在 Entity
或 Value Object
的東西。
- 例如你可能有
User
,然後當中有個加密的密碼,可是你直接把加密的方式直接放在User
裡面,這樣你的User
有可能就違反了SRP
(Single Responsibility Principle
),這時你就可以將加密的邏輯抽離出來。
也可以用來整合多個 Aggregate
之間的行為。
- 例如你可能有一個
Aggregate
需要先看看另一個Aggregate
的狀態再來決定要不要做什麼,這時你就可以透過Domain Service
來整合這兩個Aggregate
之間的行為。
但是需要謹慎使用,因為他是最彈性的,如果用太多的話你的模型可能就會成為 貧血模型(Anemic Domain Model
),這樣就失去了 DDD
的意義。
註: 貧血模型: 就是你的 Model
只有屬性,沒有行為,這樣就失去了 DDD
的意義。
舉例來說,一般以前所寫的 MVC
,當中的 Model
可能只有屬性(getter
、setter
等),而行為都放在 Controller
裡面,這樣就是一個貧血模型。
至於怎麼分辨什麼能放 Domain Service
什麼不能? 記住一件事,他的名字是 Domain Service
,所以他的行為應該是屬於 Domain
的行為。
Application Service
Application Service
是一個用來處理 Application
的行為,他是一個 Facade
,他會去協調 Domain
或相關元件的行為,所以本身是沒有處理邏輯的。
應該將 Application Service
設計的很薄,類似 Usecase
的概念。
這裡一樣附上一張圖:
圖片來源 - Domain-Driven Design: Tackling Complexity in the Heart of Software
可以看到說 Application Service
會去協調 Domain
和 Infrastructure
的行為,然後他本身是沒有額外的處理邏輯的。
Domain Event
Domain Event
是一個用來捕捉 Domain
當中「已經」發生的事,所以命名上應該要用過去式。
而這個 Event
通常是由 Aggregate
來發送的,他會去通知其他的 Aggregate
進行一些行為。
其中一種實作方式是發布訂閱(Publish/Subscribe
) 的機制,這樣你就可以很容易的去通知其他的 Aggregate
進行一些行為。
而既然變成了 Event Driven
,那你就可以很容易的去做一些非同步的行為,例如寄信、發簡訊等等。
接著既然是非同步的行為,那麼就沒辦法保證強一致性,只能保證最終一致性,所以就要去考慮這個問題並做相應的處理。
然後盡可能的將事件處理的部分設計成 Idempotent
,這樣就可以避免重複處理的問題。
最後是也不要在 Event
身上放太多的資訊,只要放最少需要的資訊,否則不只是 Event
本身會變得很大,你的 Event
可能也會失去原本的意義。
實作方式
DDD
的實作方式有很多種,其中一種是 Clean Architecture
,但這篇實在是太長了,加上 Clean Architecture
也是一個獨立的主題,所以我打算將 Clean Architecture
的部分放在下一篇文章。
結語
這篇文章主要是在介紹 DDD
的一些基本概念(戰略設計、戰術設計)。
希望這篇文章能夠幫助你了解 DDD
的一些基本概念,如果我有說錯的地方,也請不吝指教,因為我也還正在學習中。
至於如何將 DDD
落實到實際的專案中,這部分可能要等之後先將 Clean Architecture
的部分寫完之後再來寫。(過程中可能還需要先補足其他文章,才有辦法寫一篇比較完整的文章)