識別實體與值對象的特征
甄別實體與值對象非常重要,正確與否會直接影響聚合的設計。
聚合是邊界
在DDD中,聚合是實體與值對象的邊界。一個聚合對外代表了一個完整的領域概念,遵循面向對象設計的基本原則,聚合內部往往由多個細小的高內聚領域概念組成。聚合內部的領域模型形成了一棵樹,樹的根必須是實體,可以稱之為是聚合根(Aggregate Root),當然,也可以稱之為根實體(Root Entity),它是聚合的唯一入口或出口。例如訂單聚合定義了Order根實體,它就是訂單聚合的唯一代言人。
在一個限界上下文的所有領域模型(實體和值對象)中,按照關系的強弱與概念的完整性,將其劃分為多個聚合,就好像草原部落由一個個蒙古包構成了松散的聚居社群一般。
考慮到值對象與實體的差異,倘若需要管理它們的生命周期,則值對象不可能脫離聚合的邊界單獨存在。這就意味著,當我們要識別領域模型的聚合時,實體與值對象之間的強弱關系并不會影響到對聚合邊界的界定。只要實體與值對象之間存在關系,無論關系強弱,該值對象都必須與存在關系的實體放在同一個聚合。如果一個值對象與多個實體之間存在關系,要么說明多個實體都屬于一個聚合;要么意味著該值對象需要復制為多份,放到不同的聚合中,如下圖所示:
如此一來,對于聚合邊界的識別,就變成了對實體關系強弱的判斷。只要我們正確地甄別了實體與值對象,在識別聚合時,就可以不再考慮值對象,如此就能降低識別的難度。
上下文的影響
雖然我們知道實體與值對象之間的本質差異在于是否具備唯一的身份標識(identity),然而許多時候,這一差異仍然顯得似是而非。更何況,實體與值對象的定義并非絕對,在不同的上下文,同一個領域概念也可能定義為不同的設計類型。例如下圖所示的鈔票一枚:
在購買上下文,買賣雙方只關注鈔票的面值與貨幣類型,只要值相等,即可認為是同一個對象,因而需定義為值對象;在印鈔上下文,每張鈔票都具有一個唯一的標識,即使同為100元的人民幣,只要ID不同,也會認為是不同的對象,故而定義為實體。因此,要正確地甄別實體與值對象,需要結合具體的上下文。
識別的特征
即便如此,仍然缺乏相對客觀的判斷標準。為此,我總結了如下幾個特征。
相等性
甄別實體與值對象,可以首先從相等性進行判斷。只要一個領域模型對象的屬性值相等,就認為是同一個對象,應優先考慮建模為值對象;否則,需要為領域模型對象定義唯一標識,并建模為實體。
注意:在進行相等性判斷時,不能將作為唯一標識的ID視為領域模型的屬性。
例如地址領域概念,只要其屬性值國家、省份、城市、街道與郵政編碼相等,就可以認為是同一個地址,應將Address類定義為值對象。對于大家耳熟能詳的訂單領域概念,顯然需要為其分配一個唯一的訂單編號,因為理論上可能存在除訂單編號外其他屬性都相同的兩個不同訂單,應將Order定義為實體。
然而,在對相等性進行判斷時,可能出現ID與屬性存在一種隱含的對應關系。例如,出版行業中作為正規出版物的圖書,具有唯一的ISBN號,它相當于是圖書領域概念的ID,所以Book應定義為實體??稍趯ook相等性進行判斷時,也可以不通過ISBN進行相等性判斷,基本上,只要書名、作者(譯者)、出版社、價格、出版日期、版次、頁數、字數等屬性值相同,也可以認為是同一本書,那是否意味著可以將Book定義為值對象呢?
顯然,在進行相等性判斷時,考慮的屬性越多,就會出現多個組合的屬性形成一種“隱藏”的唯一標識特征,有一些體現業務規則的ID,自身就是根據屬性值來定義的。例如,航班的唯一標識就可以根據承運公司二字碼、航班號、起降機場三字碼與執飛日期來決定。通過唯一標識固然可以決定是否同一個航班,根據映射的多個屬性值,也可以判斷相等性。這會讓人在甄別實體與值對象時,顯得搖擺不定。例如,騰訊會議的會議號是Meeting的身份標識,在比較會議的相等性時,倘若我們考慮了除會議號之外的其他屬性,如會議名稱、會議類型、開始時間、結束時間、創建人、創建時間等屬性,不一樣可以確定會議的相等性嗎?
因此,除了判斷相等性,還需考慮不變性。
不變性
Eric Evans建議將值對象定義為不變的類,實則是因為根據值判等的值對象就應該具有不變性。仍以購買上下文的鈔票為例,50元+50元=100元,這100元與原來的50元是另一張不同的鈔票:
反之,一個對象除了ID,其余屬性值都可以修改,不需要創建一個新的對象,就可以認為該領域對象是可變的,應考慮定義為實體。如前所述的Meeting對象,只要meetingId值不變,如會議名稱、會議類型、開始時間、結束時間這樣的屬性值即使發生了天翻地覆的變化,我們也認為它是同一個會議。顯然,應將Meeting定義為實體。
再考慮一個典型的訂單聚合:
為什么我們要將訂單聚合中的OrderItem定義為實體?如果不考慮ID屬性,只要orderId、product與quantity值相同,完全可以認為是同一個訂單項。然則,訂單項的quantity值是可以更改的,更改了數量的訂單項也不會認為是不同的訂單項。訂單項的可變性決定了它應該定義為實體。
為何要將OrderItem的Product屬性定義為值對象呢?要知道,該Product類型還定義了productId屬性,既然具有身份標識,不應該定義為實體嗎?因為在訂單上下文中,商品的productId來自于商品上下文的商品ID,在訂單聚合中,可以將productId視為Product類的屬性值。只要productId、name和price值相同,就可以認為是同一個商品,且它們的值是不變的。這正是將Product定義為值對象的原因所在。
獨立性
即使考慮了相等性和不變性,仍有一種例外情形,那就是考慮獨立性特征。值對象作為實體的屬性必定附屬于實體,不能單獨存在;如果一個領域對象既滿足了相等性,又滿足了不變性,可定義為值對象;可是,如果它單獨存在,且需要管理其生命周期,就需要將這樣的類“升級”為實體。
考慮考勤上下文的假期領域概念。由于中國農歷假期的緣故,每年都需要配置新的假期。假期概念對應的Holiday類定義為:
顯然,該類的所有屬性值相等,即可認為是同一個假期,一旦修改了假期的值,也可以認為是不同的假期,即Holiday類同時滿足相等性和不變性,應定義為值對象??墒?,在考勤上下文的領域模型中,Holiday類是完全獨立的,不依附于其他任何實體,而它也需要管理生命周期。這時,就應遵循獨立性特征,將其“升級”為實體。
優先級
以上三個特征并無重要性排列,需綜合考慮。如果仍然無法判斷,就遵循優先級原則:優先將領域概念建模為值對象。