實體與價值物件的比較
為了定義實體和值物件之間的差異,我們需要引入三種類型的相等性,當我們需要將物件相互比較時,它們會起作用。
- 引用相等意味著如果兩個物件引用記憶體中的相同地址,則認為它們是相等的。
- 識別符號相等性意味著類具有id欄位。如果具有相同的識別符號,則此類的兩個例項將是相等。
- 結構相等的情況下,如果所有成員都匹配,我們認為兩個物件相等。
實體和值物件之間的主要區別在於我們將它們的例項相互比較的方式。識別符號平等的概念指的是實體,而結構平等的概念指的是價值物件。換句話說,實體具有固有的標識,而價值物件則沒有。
實際上,這意味著值物件沒有識別符號欄位,如果兩個值物件具有相同的屬性集,我們可以互換地處理它們。同時,如果兩個實體例項中的資料相同(Id屬性除外),我們認為它們不等同。
您可以通過類似的方式來考慮它,您會想到兩個同名的人。因此,您不會將他們視為同一個人。兩個人都有自己的固有標識。然而,如果一個人有1美元的鈔票,他們不在乎這張實物紙是否和昨天一樣。直到它仍然是1美元,他們可以用另一個替換這個音符。在這種情況下,貨幣的概念將是一個價值物件。
實體與值物件:生命週期
這兩個概念之間的另一個區別是它們例項的壽命。實體生活在連續體中,可以這麼說。他們有發生在他們身上的歷史以及他們在一生中如何改變的歷史(即使我們不儲存它)。
值物件同時具有零生命週期。我們輕鬆創造並摧毀它們。這是可互換的必然結果。如果這1美元的鈔票與另一個賬單相同,為什麼還要費心呢?我們可以用我們剛剛例項化的物件替換現有物件,完全忘記它。
從這種區別流出的指導原則是價值物件不能靠自己生活,它們應該總是屬於一個或多個實體。值物件表示的資料僅在其引用的實體的上下文中具有含義。在上面的人和金錢的例子中,“多少錢?”這個問題沒有任何意義,因為它沒有傳達適當的背景。與此同時,“彼得有多少錢?”或“我們所有使用者擁有多少錢?”這些問題完全有效。
這裡的另一個推論是我們不單獨儲存值物件。我們持久儲存值物件的唯一方法是將它附加到實體(一分鐘內更多關於它)。
實體與值物件:不變性
下一個區別是不變性。值物件應該是不可變的,如果我們需要更改這樣的物件,我們構建一個基於現有物件的新例項而不是更改它。相反,實體幾乎總是可變的。
值物件是否應該永遠是不可改變的問題是爭議的主題。一些程式員認為這個規則不像前一個規則那麼嚴格,並且在某些情況下,值物件確實是可變的。我過去也堅持這種觀點。
如今,我發現不變性與用另一個值替換價值物件的能力之間的聯絡更深刻。通過改變值物件的例項,您可以假設它具有自己的生命週期。反過來,這個假設得出結論,值物件有其固有的標識,這與DDD概念的定義相矛盾。
這種簡單的心理練習使不變性成為值物件的內在組成部分。如果我們接受值物件的生命週期為零,這意味著它們只是某些狀態的快照,而不是更多,那麼我們必須承認它們只能代表該狀態的單個變體。
這導致我們遵循以下經驗法則:如果您不能使值物件不可變,那麼它不是值物件。
如何識別域模型中的值物件?
您的域模型中的概念是實體還是值物件並不總是很清楚。不幸的是,沒有客觀的屬性可以用來了解它。概念是否是值物件完全取決於問題域:概念可以是一個域模型中的實體,在另外一個上下文中則是一個值物件。
在上面的例子中,我們可以互換地對待金錢,這使得這個概念成為一個值物件。同時,如果我們建立一個跟蹤整個國家現金流量的軟體,我們需要分別處理每一個賬單,以收集每個賬單的統計資料。在這種情況下,貨幣的概念將是一個實體,雖然我們可能會將其命名為Note或Bill。
儘管缺乏客觀特徵,您仍然可以使用一些技術將概念歸因於實體或值物件。我們討論了標識的概念:如果你可以安全地將一個類的例項替換為另一個具有相同屬性集的例項,那麼這個概念就是一個值物件。
該技術的更簡單版本是將值物件與整數進行比較。你是否真的關心整數5是否與你在另一種方法中使用的5相同?絕對不是,你的應用程式中的所有五個都是相同的,無論它們如何被例項化。這使得整數本質上是一個值物件。現在,問問自己,你域中的這個概念看起來像整數嗎?如果答案是肯定的,那麼它就是一個值物件。
如何在資料庫中儲存值物件?
假設我們的域模型中有兩個類:Person實體和Address 值物件:
<font><i>// Entity</i></font><font> <b>public</b> <b>class</b> Person { <b>public</b> <b>int</b> Id { get; set; } <b>public</b> string Name { get; set; } <b>public</b> Address Address { get; set; } } </font><font><i>// Value Object</i></font><font> <b>public</b> <b>class</b> Address { <b>public</b> string City { get; set; } <b>public</b> string ZipCode { get; set; } } </font>
在這種情況下,資料庫結構將如何?想到的一個選項是為每個選項建立單獨的表。
儘管從資料庫的角度來看這種設計完全有效,但這種設計有兩個主要缺點。首先,Address表包含一個識別符號。這意味著我們必須在Address值物件中引入一個單獨的Id欄位才能正確使用這樣的表。反過來,這意味著我們為Address類提供了一些標識。這違反了值物件的定義。
另一個缺點是,使用此解決方案,我們可能會從實體中分離值物件。Address值物件現在可以獨立生存,因為我們可以從資料庫中刪除Person行而不刪除相應的Address行。這將違反另一條規則,該規則規定值物件的生命週期應完全取決於其父實體的生命週期。
事實證明,最好的解決方案是將Address表中的欄位內聯到Person表中。
這將解決我之前提到的所有問題:地址不再具有身份,其生命週期現在完全取決於Person實體的生命週期。
如果您在心理上用我之前建議的單個整數替換關於Address的欄位,這個設計也是有意義的。你是否為整數建立了一個單獨的表?當然不是,您只需將該行的整數內聯到您希望它所在的表中。這同樣適用於值物件。不要為值物件引入單獨的表,只需將它們內聯到父實體的表中即可。
首選值物件
在處理實體和值物件時,一個重要的指導方針發揮作用:總是更喜歡值物件而不是實體。值物件是不可變的,比實體更輕量級。因此,它們非常容易使用。理想情況下,您應該始終將大多數業務邏輯放入值物件中。在這種情況下,實體將充當它們的包裝並代表更高級別的功能。
此外,您最初看作實體的概念可能基本上是一個值物件。例如,程式碼庫中的Address類最初可以作為實體引入。它可能有自己的Id欄位和資料庫中的單獨表。重新訪問後,您可能會注意到,在您的域中,地址實際上並沒有自己的固有標識,可以互換使用。在這種情況下,請不要猶豫重構您的域模型並將實體轉換為值物件。
總結
好吧,我想我涵蓋了與實體和價值物件相關的所有方面。我們用以下內容總結一下:
- 實體有自己的內在標識,值物件沒有。
- 標識平等的概念是指實體; 結構平等的概念指的是價值客體;引用平等的概念指的是兩者。
- 實體有歷史; 值物件的生命週期為零。
- 值物件應始終屬於一個或多個實體,它不能獨立存在。
- 值物件應該是不可變的; 實體幾乎總是可變的。
- 要識別域模型中的值物件,請在心理上將其替換為整數。
- 值物件不應在資料庫中擁有自己的表。
- 始終優先考慮域模型中實體的值物件。