DDD中實體與值物件是幹什麼的

等不到的口琴發表於2021-06-06

實體值物件的含義

我們前面已經講過領域的概念, 今天來講講實體, 實體是我們進行設計領域模型時的基礎單元, 與之有關的是值物件, 接下來先梳理一下實體以及值物件的含義,然後講講他們倆的關係, 希望通過這篇文章能讓你區分開與傳統架構中實體的區別, 對領域模型中的實體有進一步的瞭解。

實體

實體這個概念在傳統微服務中也有涉及, 在傳統技術中, 實體一般是指實體類, 也就是資料庫中的表格, 但是在領域模型中實體與傳統架構中的實體有類似的地方但是不完全一樣, 在DDD中, 實體的定義是: 實體擁有唯一識別符號, 且識別符號在經歷過各種狀態變更後仍能保持一致。對這些物件而言, 重要的不是其屬性,而是其延續性和標識,物件的延續性和標識會跨越甚至超出軟體的生命週期。下面舉一些例子:

實體的業務形態

在 DDD 不同的設計過程中,實體的形態是不同的。在戰略設計時,實體是領域模型的一個重要物件。領域模型中的實體是多個屬性、操作或行為的載體。到這兒是不是就有一點感覺了, 我們通常的實體中一般不會有行為以及載體, 但是領域模型中的實體會有行為以及載體在, 事件風暴中,我們可以根據命令、操作或者事件,找出產生這些行為的業務實體物件,進而按照一定的業務規則將依存度高和業務關聯緊密的多個實體物件和值物件進行聚類,形成聚合。可以這麼理解,實體和值物件是組成領域模型的基礎單元。

實體程式碼形態

在程式碼模型中,實體的表現形式是實體類,這個類包含了實體的屬性方法,通過這些方法實現實體自身的業務邏輯。在 DDD 裡,這些實體類通常採用充血模型,與這個實體相關的所有業務邏輯都在實體類的方法中實現,跨多個實體的領域邏輯則在領域服務中實現。

充血模型和貧血模型, 其實就是一個相對的概念, 在我們之前的MVC架構設計中, 一般資料放在資料層, 也就是實體中只包含屬性, 對應的行為放在服務層, 這種實體不包含任何行為只包含屬性的模型稱之為貧血模型, 而充血模型就是實體中包含屬性與行為。

實體執行時態

實體以 DO(領域物件)的形式存在,每個實體物件都有唯一的 ID。我們可以對一個實體物件進行多次修改,修改後的資料和原來的資料可能會大不相同。但是,由於它們擁有相同的 ID,它們依然是同一個實體。比如商品是商品上下文的一個實體,通過唯一的商品 ID 來標識,不管這個商品的資料如何變化,商品的 ID 一直保持不變,它始終是同一個商品。

實體資料庫形態

與傳統資料模型設計優先不同,DDD 是先構建領域模型,針對實際業務場景構建實體物件和行為,再將實體物件對映到資料持久化物件。在領域模型對映到資料模型時,一個實體可能對應 0 個、1 個或者多個資料庫持久化物件。大多數情況下實體與持久化物件是一對一。在某些場景中,有些實體只是暫駐靜態記憶體的一個執行態實體,它不需要持久化。比如,基於多個價格配置資料計算後生成的折扣實體, 這個只是一個數值, 不需要持久化到持久層。

而在有些複雜場景下,實體與持久化物件則可能是一對多或者多對一的關係。比如,使用者user 與角色 role 兩個持久化物件可生成許可權實體,一個實體對應兩個持久化物件,這是一對多的場景。再比如,有些場景為了避免資料庫的聯表查詢,提升系統效能,會將客戶資訊customer 和賬戶資訊 account 兩類資料儲存到同一張資料庫表中,客戶和賬戶兩個實體可根據需要從一個持久化物件中生成,這就是多對一的場景。

值物件

值物件相對實體來說,會更加抽象一些,概念上我們會結合例子來講。

我們先看一下《實現領域驅動設計》一書中對值物件的定義:通過物件屬性值來識別的物件,它將多個相關屬性組合為一個概念整體。在 DDD 中用來描述領域的特定方面,並且是一個沒有識別符號的物件,叫作值物件。也就說,值物件描述了領域中的一件東西,這個東西是不可變的,它將不同的相關屬性組合成了一個概念整體。當度量和描述改變時,可以用另外一個值物件予以替換。它可以和其它值物件進行相等性比較,且不會對協作物件造成副作用。這部分在後面講“值物件的執行形態”時還會有例子。
上面這兩段對於定義的闡述,如果你還是覺得有些晦澀,我們不妨“翻譯”一下,用更通俗的語言把定義講清楚。

簡單來說,值物件本質上就是一個集合。那這個集合裡面有什麼呢?若干個用於描述目的、具有整體概念和不可修改的屬性。那這個集合存在的意義又是什麼?在領域建模的過程中,值物件可以保證屬性歸類的清晰和概念的完整性,避免屬性零碎。

例如地址, 我們都知道中國的地址是由省市區(縣)組成的, 一般來說[省、市、區]就可以看成一個值物件用來描述實體的地址, 如果我今天在老家, 那麼值物件就是[湖北、武漢、光谷], 現在來深圳了, 那麼值物件就是[廣東、深圳、福田] , 那麼也就意味著描述地址的值物件是窮盡的, 如果需要更改的話, 只能用一個值物件替換另一個值物件。

人員實體原本包括:姓名、年齡、性別以及人員所在的省、市、縣和街道等屬性。這樣顯示地址相關的屬性就很零碎了對不對?現在,我們可以將“省、市、縣和街道等屬性”拿出來構成一個“地址屬性集合”,這個集合就是值物件了。

值物件的業務形態

值物件是 DDD 領域模型中的一個基礎物件,它跟實體一樣都來源於事件風暴所構建的領域模型,都包含了若干個屬性,它與實體一起構成聚合。我們不妨對照實體,來看值物件的業務形態,這樣更好理解。本質上,實體是看得到、摸得著的實實在在的業務物件,實體具有業務屬性、業務行為和業務邏輯。而值物件只是若干個屬性的集合,只有資料初始化操作和有限的不涉及修改資料的行為,基本不包含業務邏輯。值物件的屬性集雖然在物理上獨立出來了,但在邏輯上它仍然是實體屬性的一部分,用於描述實體的特徵。在值物件中也有部分共享的標準型別的值物件,它們有自己的限界上下文,有自己的持久化物件,可以建立共享的資料類微服務,比如資料字典。

值物件的程式碼形態

值物件在程式碼中有這樣兩種形態。如果值物件是單一屬性,則直接定義為實體類的屬性;如果值物件是屬性集合,則把它設計為 Class 類,Class 將具有整體概念的多個屬性歸集到屬性集合,這樣的值物件沒有 ID,會被實體整體引用。我們看一下下面這段程式碼,person 這個實體有若干個單一屬性的值物件,比如 Id、name等屬性;同時它也包含多個屬性的值物件,比如地址 address。

值物件的執行形態

實體例項化後的 DO 物件的業務屬性和業務行為非常豐富,但值物件例項化的物件則相對簡單和乏味。除了值物件資料初始化和整體替換的行為外,其它業務行為就很少了。值物件嵌入到實體的話,有這樣兩種不同的資料格式,也可以說是兩種方式,分別是屬性嵌入的方式和序列化大物件的方式。引用單一屬性的值物件或只有一條記錄的多屬性值物件的實體,可以採用屬性嵌入的方式嵌入。引用一條或多條記錄的多屬性值物件的實體,可以採用序列化大物件的方式嵌入。比如,人員實體可以有多個通訊地址,多個地址序列化後可以嵌入人員的地址屬性。值物件建立後就不允許修改了,只能用另外一個值物件來整體替換。

如果你對這兩種方式不夠了解,可以看看下面的例子。

案例 1:以屬性嵌入的方式形成的人員實體物件,地址值物件直接以屬性值嵌入人員實體中。

案例 2:以序列化大物件的方式形成的人員實體物件,地址值物件被序列化成大物件 Json串後,嵌入人員實體中。

值物件的資料庫形態

DDD 引入值物件是希望實現從“資料建模為中心”向“領域建模為中心”轉變,減少資料庫表的數量和表與表之間複雜的依賴關係,儘可能地簡化資料庫設計,提升資料庫效能。如何理解用值物件來簡化資料庫設計呢?

傳統的資料建模大多是根據資料庫正規化設計的,每一個資料庫表對應一個實體,每一個實體的屬性值用單獨的一列來儲存,一個實體主表會對應 N 個實體從表。而值物件在資料庫持久化方面簡化了設計,它的資料庫設計大多采用非資料庫正規化,值物件的屬性值和實體物件的屬性值儲存在同一個資料庫實體表中。

舉個例子,還是基於上述人員和地址那個場景,實體和資料模型設計通常有兩種解決方案:

第一是把地址值物件的所有屬性都放到人員實體表中,建立人員實體,建立人員資料表;

第二是建立人員和地址兩個實體,同時建立人員和地址兩張表。

第一個方案會破壞地址的業務涵義和概念完整性,第二個方案增加了不必要的實體和表,需要處理多個實體和表的關係,從而增加了資料庫設計的複雜性。那到底應該怎樣設計,才能讓業務含義清楚,同時又不讓資料庫變得複雜呢?我們可以綜合這兩個方案的優勢,揚長避短。在領域建模時,我們可以把地址作為值物件,人員作為實體,這樣就可以保留地址的業務涵義和概念完整性。而在資料建模時,我們可以將地址的屬性值嵌入人員實體資料庫中,只建立人員資料庫表。這樣既可以兼顧業務含義和表達,又不增加資料庫的複雜度。值物件就是通過這種方式,簡化了資料庫設計

總結一下就是:在領域建模時,我們可以將部分物件設計為值物件,保留物件的業務涵義,同時又減少了實體的數量;在資料建模時,我們可以將值物件嵌入實體,減少實體表的數量,簡化資料庫設計。另外,也有 DDD 專家認為,要想發揮物件的威力,就需要優先做領域建模,弱化資料庫的作用,只把資料庫作為一個儲存資料的倉庫即可。即使違反資料庫設計原則,也不用大驚小怪,只要業務能夠順利執行,就沒什麼關係。

值物件的優勢和侷限

值物件是一把雙刃劍,它的優勢是可以簡化資料庫設計,提升資料庫效能。但如果值物件使用不當,它的優勢就會很快變成劣勢。“知彼知己,方能百戰不殆”,你需要理解值物件真正適合的場景。值物件採用序列化大物件的方法簡化了資料庫設計,減少了實體表的數量,可以簡單、清晰地表達業務概念。這種設計方式雖然降低了資料庫設計的複雜度,但卻無法滿足基於值物件的快速查詢,會導致搜尋值物件屬性值變得異常困難。值物件採用屬性嵌入的方法提升了資料庫的效能,但如果實體引用的值物件過多,則會導致實體堆積一堆缺乏概念完整性的屬性,這樣值物件就會失去業務涵義,操作起來也不方便。所以,你可以對照著以上這些優劣勢,結合你的業務場景,好好想一想了。那如果在你的業務場景中,值物件的這些劣勢都可以避免掉,那就請放心大膽地使用值物件吧。

實體和值物件的關係

實體和值物件是微服務底層的最基礎的物件,一起實現實體最基本的核心領域邏輯。值物件和實體在某些場景下可以互換,很多 DDD 專家在這些場景下,其實也很難判斷到底將領域物件設計成實體還是值物件?可以說,值物件在某些場景下有很好的價值,但是並不是所有的場景都適合值物件。你需要根據團隊的設計和開發習慣,以及上面的優勢和侷限分析,選擇最適合的方法。關於值物件,我還要多說幾句。其實,DDD 引入值物件還有一個重要的原因,就是到底領域建模優先還是資料建模優先?DDD 提倡從領域模型設計出發,而不是先設計資料模型。前面講過了,傳統的資料模型設計通常是一個表對應一個實體,一個主表關聯多個從表,當實體表太多的時候就很容易陷入無窮無盡的複雜的資料庫設計,領域模型就很容易被資料模型綁架。可以說,值物件的誕生,在一定程度上,和實體是互補的。我們還是以前面的圖示為例:

在領域模型中人員是實體,地址是值物件,地址值物件被人員實體引用。在資料模型設計時,地址值物件可以作為一個屬性集整體嵌入人員實體中,組合形成上圖這樣的資料模型;也可以以序列化大物件的形式加入到人員的地址屬性中,前面表格有展示。從這個例子中,我們可以看出,同樣的物件在不同的場景下,可能會設計出不同的結果。有些場景中,地址會被某一實體引用,它只承擔描述實體的作用,並且它的值只能整體替換,這時候你就可以將地址設計為值物件,比如收貨地址。而在某些業務場景中,地址會被經常修改,地址是作為一個獨立物件存在的,這時候它應該設計為實體,比如行政區劃中的地址資訊維護。

總結

今天我們主要學習了實體和值物件在 DDD 不同設計階段的形態,以及它們從戰略設計向戰術設計演進過程中的設計方法。這個過程是從業務模型向系統模型落地的過程,比較複雜,很考驗你的設計能力,很多時候我們都要結合自己的業務場景,選擇合適的方法來進行微服務設計。強調一點,我們不避諱傳統的設計方法,畢竟適合自己的才是最好的。希望你能充分理解實體和值物件的概念和應用,將學到的知識複用,最終將適合自己業務的 DDD 設計方法納入到架構體系,實現落地。

站在巨人的肩膀上

  1. 極客時間歐創新DDD實踐課
  2. 田園裡的蟋蟀我的領域驅動設計之路
  3. dax.net領域驅動設計實踐

相關文章