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