今天我們來學習 DDD 戰術設計中的兩個重要概念:實體和值對象。
這兩個概念都是領域模型中的領域對象。它們在領域模型中起什么作用,戰術設計時如何將它們映射到代碼和數據模型中去?就是我們這一講重點要關注的問題。
另外,在戰略設計向戰術設計過渡的這個過程中,理解和區分實體和值對象在不同階段的形態是很重要的,畢竟階段不同,它們的形態也會發生變化,這與我們的設計和代碼實現密切相關。
接下來,我們就分別看看實體和值對象的這些問題,從中找找答案。
實體
我們先來看一下實體是什么東西?
在 DDD 中有這樣一類對象,它們擁有唯一標識符,且標識符在歷經各種狀態變更后仍能保持一致。對這些對象而言,重要的不是其屬性,而是其延續性和標識,對象的延續性和標識會跨越甚至超出軟件的生命周期。我們把這樣的對象稱為實體。沒理解?沒關系!請繼續閱讀。
1. 實體的業務形態
在 DDD 不同的設計過程中,實體的形態是不同的。在戰略設計時,實體是領域模型的一個重要對象。領域模型中的實體是多個屬性、操作或行為的載體。在事件風暴中,我們可以根據命令、操作或者事件,找出產生這些行為的業務實體對象,進而按照一定的業務規則將依存度高和業務關聯緊密的多個實體對象和值對象進行聚類,形成聚合。你可以這么理解,實體和值對象是組成領域模型的基礎單元。
2. 實體的代碼形態
在代碼模型中,實體的表現形式是實體類,這個類包含了實體的屬性和方法,通過這些方法實現實體自身的業務邏輯。在 DDD 里,這些實體類通常采用充血模型,與這個實體相關的所有業務邏輯都在實體類的方法中實現,跨多個實體的領域邏輯則在領域服務中實現。
3. 實體的運行形態
實體以 DO(領域對象)的形式存在,每個實體對象都有唯一的 ID。我們可以對一個實體對象進行多次修改,修改后的數據和原來的數據可能會大不相同。但是,由于它們擁有相同的 ID,它們依然是同一個實體。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標識,不管這個商品的數據如何變化,商品的 ID 一直保持不變,它始終是同一個商品。
4. 實體的數據庫形態
與傳統數據模型設計優先不同,DDD 是先構建領域模型,針對實際業務場景構建實體對象和行為,再將實體對象映射到數據持久化對象。
在領域模型映射到數據模型時,一個實體可能對應 0 個、1 個或者多個數據庫持久化對象。大多數情況下實體與持久化對象是一對一。在某些場景中,有些實體只是暫駐靜態內存的一個運行態實體,它不需要持久化。比如,基于多個價格配置數據計算后生成的折扣實體。
而在有些復雜場景下,實體與持久化對象則可能是一對多或者多對一的關系。比如,用戶 user 與角色 role 兩個持久化對象可生成權限實體,一個實體對應兩個持久化對象,這是一對多的場景。再比如,有些場景為了避免數據庫的聯表查詢,提升系統性能,會將客戶信息 customer 和賬戶信息 account 兩類數據保存到同一張數據庫表中,客戶和賬戶兩個實體可根據需要從一個持久化對象中生成,這就是多對一的場景。
值對象
值對象相對實體來說,會更加抽象一些,概念上我們會結合例子來講。
我們先看一下《實現領域驅動設計》一書中對值對象的定義:通過對象屬性值來識別的對象,它將多個相關屬性組合為一個概念整體。在 DDD 中用來描述領域的特定方面,并且是一個沒有標識符的對象,叫作值對象。
也就說,值對象描述了領域中的一件東西,這個東西是不可變的,它將不同的相關屬性組合成了一個概念整體。當度量和描述改變時,可以用另外一個值對象予以替換。它可以和其它值對象進行相等性比較,且不會對協作對象造成副作用。這部分在后面講“值對象的運行形態”時還會有例子。
上面這兩段對于定義的闡述,如果你還是覺得有些晦澀,我們不妨“翻譯”一下,用更通俗的語言把定義講清楚。
簡單來說,值對象本質上就是一個集。那這個集合里面有什么呢?若干個用于描述目的、具有整體概念和不可修改的屬性。那這個集合存在的意義又是什么?在領域建模的過程中,值對象可以保證屬性歸類的清晰和概念的完整性,避免屬性零碎。
這里我舉個簡單的例子,請看下面這張圖:
人員實體原本包括:姓名、年齡、性別以及人員所在的省、市、縣和街道等屬性。這樣顯示地址相關的屬性就很零碎了對不對?現在,我們可以將“省、市、縣和街道等屬性”拿出來構成一個“地址屬性集合”,這個集合就是值對象了。
1. 值對象的業務形態
值對象是 DDD 領域模型中的一個基礎對象,它跟實體一樣都來源于事件風暴所構建的領域模型,都包含了若干個屬性,它與實體一起構成聚合。
我們不妨對照實體,來看值對象的業務形態,這樣更好理解。本質上,實體是看得到、摸得著的實實在在的業務對象,實體具有業務屬性、業務行為和業務邏輯。而值對象只是若干個屬性的集合,只有數據初始化操作和有限的不涉及修改數據的行為,基本不包含業務邏輯。值對象的屬性集雖然在物理上獨立出來了,但在邏輯上它仍然是實體屬性的一部分,用于描述實體的特征。
在值對象中也有部分共享的標準類型的值對象,它們有自己的限界上下文,有自己的持久化對象,可以建立共享的數據類微服務,比如數據字典。
2. 值對象的代碼形態
值對象在代碼中有這樣兩種形態。如果值對象是單一屬性,則直接定義為實體類的屬性;如果值對象是屬性集合,則把它設計為 Class 類,Class 將具有整體概念的多個屬性歸集到屬性集合,這樣的值對象沒有 ID,會被實體整體引用。
我們看一下下面這段代碼,person 這個實體有若干個單一屬性的值對象,比如 Id、name 等屬性;同時它也包含多個屬性的值對象,比如地址 address。
3. 值對象的運行形態
實體實例化后的 DO 對象的業務屬性和業務行為非常豐富,但值對象實例化的對象則相對簡單和乏味。除了值對象數據初始化和整體替換的行為外,其它業務行為就很少了。
值對象嵌入到實體的話,有這樣兩種不同的數據格式,也可以說是兩種方式,分別是屬性嵌入的方式和序列化大對象的方式。
引用單一屬性的值對象或只有一條記錄的多屬性值對象的實體,可以采用屬性嵌入的方式嵌入。引用一條或多條記錄的多屬性值對象的實體,可以采用序列化大對象的方式嵌入。比如,人員實體可以有多個通訊地址,多個地址序列化后可以嵌入人員的地址屬性。值對象創建后就不允許修改了,只能用另外一個值對象來整體替換。
如果你對這兩種方式不夠了解,可以看看下面的例子。
案例 1:以屬性嵌入的方式形成的人員實體對象,地址值對象直接以屬性值嵌入人員實體中。
案例 2:以序列化大對象的方式形成的人員實體對象,地址值對象被序列化成大對象 Json 串后,嵌入人員實體中。
4. 值對象的數據庫形態
DDD 引入值對象是希望實現從“數據建模為中心”向“領域建模為中心”轉變,減少數據庫表的數量和表與表之間復雜的依賴關系,盡可能地簡化數據庫設計,提升數據庫性能。
如何理解用值對象來簡化數據庫設計呢?
傳統的數據建模大多是根據數據庫范式設計的,每一個數據庫表對應一個實體,每一個實體的屬性值用單獨的一列來存儲,一個實體主表會對應 N 個實體從表。而值對象在數據庫持久化方面簡化了設計,它的數據庫設計大多采用非數據庫范式,值對象的屬性值和實體對象的屬性值保存在同一個數據庫實體表中。
舉個例子,還是基于上述人員和地址那個場景,實體和數據模型設計通常有兩種解決方案:第一是把地址值對象的所有屬性都放到人員實體表中,創建人員實體,創建人員數據表;第二是創建人員和地址兩個實體,同時創建人員和地址兩張表。
第一個方案會破壞地址的業務涵義和概念完整性,第二個方案增加了不必要的實體和表,需要處理多個實體和表的關系,從而增加了數據庫設計的復雜性。
那到底應該怎樣設計,才能讓業務含義清楚,同時又不讓數據庫變得復雜呢?
我們可以綜合這兩個方案的優勢,揚長避短。在領域建模時,我們可以把地址作為值對象,人員作為實體,這樣就可以保留地址的業務涵義和概念完整性。而在數據建模時,我們可以將地址的屬性值嵌入人員實體數據庫表中,只創建人員數據庫表。這樣既可以兼顧業務含義和表達,又不增加數據庫的復雜度。
值對象就是通過這種方式,簡化了數據庫設計,總結一下就是:在領域建模時,我們可以將部分對象設計為值對象,保留對象的業務涵義,同時又減少了實體的數量;在數據建模時,我們可以將值對象嵌入實體,減少實體表的數量,簡化數據庫設計。
另外,也有 DDD 專家認為,要想發揮對象的威力,就需要優先做領域建模,弱化數據庫的作用,只把數據庫作為一個保存數據的倉庫即可。即使違反數據庫設計原則,也不用大驚小怪,只要業務能夠順利運行,就沒什么關系。
5. 值對象的優勢和局限
值對象是一把雙刃劍,它的優勢是可以簡化數據庫設計,提升數據庫性能。但如果值對象使用不當,它的優勢就會很快變成劣勢。“知彼知己,方能百戰不殆”,你需要理解值對象真正適合的場景。
值對象采用序列化大對象的方法簡化了數據庫設計,減少了實體表的數量,可以簡單、清晰地表達業務概念。這種設計方式雖然降低了數據庫設計的復雜度,但卻無法滿足基于值對象的快速查詢,會導致搜索值對象屬性值變得異常困難。
值對象采用屬性嵌入的方法提升了數據庫的性能,但如果實體引用的值對象過多,則會導致實體堆積一堆缺乏概念完整性的屬性,這樣值對象就會失去業務涵義,操作起來也不方便。
所以,你可以對照著以上這些優劣勢,結合你的業務場景,好好想一想了。那如果在你的業務場景中,值對象的這些劣勢都可以避免掉,那就請放心大膽地使用值對象吧。
實體和值對象的關系
實體和值對象是微服務底層的最基礎的對象,一起實現實體最基本的核心領域邏輯。
值對象和實體在某些場景下可以互換,很多 DDD 專家在這些場景下,其實也很難判斷到底將領域對象設計成實體還是值對象?可以說,值對象在某些場景下有很好的價值,但是并不是所有的場景都適合值對象。你需要根據團隊的設計和開發習慣,以及上面的優勢和局限分析,選擇最適合的方法。
關于值對象,我還要多說幾句。其實,DDD 引入值對象還有一個重要的原因,就是到底領域建模優先還是數據建模優先?
DDD 提倡從領域模型設計出發,而不是先設計數據模型。前面講過了,傳統的數據模型設計通常是一個表對應一個實體,一個主表關聯多個從表,當實體表太多的時候就很容易陷入無窮無盡的復雜的數據庫設計,領域模型就很容易被數據模型綁架。可以說,值對象的誕生,在一定程度上,和實體是互補的。
我們還是以前面的圖示為例:
在領域模型中人員是實體,地址是值對象,地址值對象被人員實體引用。在數據模型設計時,地址值對象可以作為一個屬性集整體嵌入人員實體中,組合形成上圖這樣的數據模型;也可以以序列化大對象的形式加入到人員的地址屬性中,前面表格有展示。
從這個例子中,我們可以看出,同樣的對象在不同的場景下,可能會設計出不同的結果。有些場景中,地址會被某一實體引用,它只承擔描述實體的作用,并且它的值只能整體替換,這時候你就可以將地址設計為值對象,比如收貨地址。而在某些業務場景中,地址會被經常修改,地址是作為一個獨立對象存在的,這時候它應該設計為實體,比如行政區劃中的地址信息維護。
總結
今天我們主要學習了實體和值對象在 DDD 不同設計階段的形態,以及它們從戰略設計向戰術設計演進過程中的設計方法。
這個過程是從業務模型向系統模型落地的過程,比較復雜,很考驗你的設計能力,很多時候我們都要結合自己的業務場景,選擇合適的方法來進行微服務設計。強調一點,我們不避諱傳統的設計方法,畢竟適合自己的才是最好的。希望你能充分理解實體和值對象的概念和應用,將學到的知識復用,最終將適合自己業務的 DDD 設計方法納入到架構體系,實現落地。