說實話,整理現在這一篇博文的想法,在上一篇發布出來的時候就有了,但到現在才動起筆來,而且寫之前又反復讀了上一篇博文的內容及評論,然后去收集資料,真正去寫的時候,才發現這類的博文真不是一般的難寫,一句話要反復揣摩,并進行理解,最重要的是半天才蹦出一句話。
看了上面的文字,你可能會覺得我是為了寫博文而寫博文,其實并不是如此,我現在覺得寫這類博文的目的在于梳理自己的觀點,然后再進行表達出來,有的人可能會覺得為什么要糾結某一類觀點?或者認為陷在一個“陷阱”中出不來,其實這只是表面如此,我的想法是通過某一類東西,去體會、學習它的過程,就像我們去某一地方旅行,你在乎到達目的地的心情嗎?其實并不盡然,你應該在意的是,在這個旅行過程中,你自己有沒有享受、體會或得到什么?這才是旅行的真正意義所在,我個人覺得這個過程對我非常有幫助,但如果把這個過程分享出來,不經意的一瞬間,對一部分朋友有所共鳴,那我覺得這是額外驚喜。
言歸正傳,上篇博文主要是通過三個問題,然后去思考實體和值對象的概念,通過實際場景去學習、理解領域模型的概念,感覺確實非常好,但第三個問題,我和 netfocus 兄在上一篇博文中探討了好久,但遺憾的是,到最后也沒準確的確定下來,這也是我寫這篇博文的一部分初衷,希望可以再次通過這個“難纏”的問題,可以更深一步的理解實體和值對象。
- 主題:消息場景中,發件人、收件人是實體?還是值對象?
發件人、收件人設計為實體會怎樣?
在上一篇博文中,第一個問題是:實體的最重要特性是什么?最后歸納為兩點:連續性(continuity)和標識(identity),然后在第三個問題分析中,結合發件人、收件人(以下用聯系人表示)是否符合或存在這兩個特性,可能在我的分析中有些牽強,所以最后我的結論是:聯系人應該設計為實體。
具體實體的兩個特性分析可以參考上一篇博文,消息場景中的業務非常簡單,其實就存在兩種“東西“:消息和聯系人,當然還有一些其他的,但都不是主要的,他們倆才是主角,這兩個東西設計的稍微不同,最后實現起來可能就會千差萬別。但首先明確一點的是,在消息場景中,聯系人是依附于消息的,脫離于消息,聯系人將毫無意義,畢竟這是消息場景,而不是人員管理場景,也可以這樣說:消息是男一號,那聯系人是男二號,并且男二號沒有“上位”的可能。
在其他的業務場景中,你會發現這種“依附”關系非常普遍,也可以說是一個應用場景最基本的關系,比如購物車場景中的 Order 和 Customer 等等,在特定的場景中,依附關系是確定的,但換一種場景,這兩者之間的關系可能就會“逆向”過來,那針對這種最普遍的關系該怎么進行設計呢?
在上篇評論中我有提到,《領域驅動設計》書中第5.3.1章設計值對象,作者列出了這樣一個關聯設計的例子:
在電力運營公司的軟件中,一個地址對應于公司線路和服務的目的地。如果多個住所都申請了電力服務,那么這個公司需要知道這一點,因此地址是實體。我們也可以用另一種方法,在模型中將“住所”關聯到運營服務,其中“住所”是一個包含地址屬性的實體。此時,地址就是一個值對象。
雖然很簡短的一段話,但信息量太大了,我覺得理解了這段話對如何設計實體和值對象非常有幫助,我們看一下后面這段話:“住所”關聯到運營服務(注意場景是電力運營),是不是有點像聯系人關聯消息呢,在電力運營場景中。“住所”的概念脫離運營服務也將毫無意義,再到后面:其中“住所”是一個包含地址屬性的實體,是不是又有點像聯系人包含名稱以及其他屬性的實體,它最后說的“此時,地址就是一個值對象”,其中的地址可以看作是聯系人的某一個屬性,比如聯系人名稱。
在另外一本 DDD 著作《實現領域驅動設計》第5章實體,作者一開始說了這樣一段話:
唯一的身份標識和可變性(mutability)特性講實體對象和值對象區分開來。
先看第一個,聯系人是否存在唯一標識?這個在上一篇博文中就已經分析了,在消息場景中,聯系人必須是唯一的,這個沒什么可爭議的,即使是另一種設計 SenderId、RecipientId,那這個值也是唯一的,這其實就是聯系人的標識,后面可變性(mutability)是什么意思呢?和上一篇博文說的連續性(continuity)有什么區別?其實我個人覺得是一個意思,值對象從應用程序一開始就創建了,并在整個過程中,它是不可變的,而實體在其自己的生命周期內,是可變的,連續性指的是實體可變的連續,它是一個過程,就像一個人從出生到死亡,在其生命過程中,他必須首先確定他是哪個人,比如可以通過身份證號進行標識,然后他自己的一些特征可能會發生變化,比如工作、生活等,這個可以看作是可變性的體現,但必須都是在唯一標識確定的前提下,這部分內容我自己表達的有些雜,可能不太好理解,大家意會就行了。
接上面,在消息場景中,最基本的業務用例是:用戶 A 給用戶 B 發一個消息,然后用戶 B 給用戶 A 回復一個消息。。。在這個過程中,我們用 SenderId、RecipientId 來區分是哪個聯系人,發送是一個動作,但在這個基本用例中,除了發送可能還會包含一些其他的東西,比如我要對聯系人進行驗證,就像我們買車票一樣,在買之前會有一些身份驗證,來確定你的身份是否合法?那這個聯系人的驗證過程是消息場景中的一部分?還是用戶場景的一部分?我覺得這是消息場景的一部分,因為針對用戶的驗證都是在發消息這個動作基礎上完成的,可以理解為這不屬于發消息,是獨立的聯系人驗證,但這個必須是在消息場景下。針對聯系人的設計之前可能只有 Id,但消息中要進行聯系人顯示啊,所以后來加了 Name,再后來又要對聯系人進行驗證,所以又加了 IsGagged。。。這是一個不斷完善的過程,這時候你會發現,在聯系人對象中,除了標識之外,其他一些屬性都是可能會變化的,也就是《實現領域驅動設計》書中所提到的可變性。
以上扯的有點“云里霧里”的感覺,回到這個標題上,聯系人設計成實體會怎樣?首先看一下 Message 消息實體中的部分代碼:
public virtual Contact Sender { get; set; }
public virtual Contact Recipient { get; set; }
Message 實體中有類型為 Contact 的 Sender、Recipient 對象,用來標識此消息的發件人和收件人,這一點沒什么問題,雖在實體中為對象關聯,但在數據庫的體現可能是 SenderId 和 RecipientId,這不是我們關心的,我們只需要操作模型中的對象即可,至于 Contact 中實體的具體設計,可以根據具體的消息場景進行設計,比如最簡單的示例代碼:
public class Contact
{
public int ID { get; set; }
public string DisplayName { get; set; }
public bool IsGagged { get; set; }
}
聯系人設計為實體,首先符合實體的一些特性,并且在消息場景中,可以更好的對聯系人進行驗證,聯系人存儲雖不在消息中進行存儲,但消息缺少聯系人同樣不行,所以針對消息中的聯系人驗證還是很有必要的,還有就是,如果哪一天消息中聯系人要單獨進行管理了,這時候首先確定的是聯系人肯定為實體,另一個重要需要考慮的是,消息和聯系人聚合問題,就像購物車中的 Orde 和 Custorm 一樣。
發件人、收件人設計為值對象會怎樣?
首先,現在的消息模型就是把聯系人設計為值對象,具體是怎么設計的,我再詳細描述下,Contact 的設計就類似上面的代碼,只不過命名空間為:CNBlogs.Msg.Domain.ValueObject,然后 Contact 和 Message 的關聯也像上面如此,只不過在存儲的時候,需要把 Contact 中所有屬性映射到 Message 中,而不只是上面的 SenderId 和 RecipientId,為什么?因為既然聯系人為值對象,那其中所有屬性值必須唯一,一個值不同或發生變化,那就是一個全新的值對象,而且值對象中的某一個屬性代表不了整個值對象。
針對上面的問題,我舉一個例子進行說明,比如 NBA 球隊之間打比賽,都是五個人之間的對抗,某一個人代表不了整個球隊,即使他再牛叉,而且一個完整的球隊,如果某一個人離開了,那這個球隊就會發生變化,對手就會根據球隊的變化做出相應的調整,這個例子可能說的有些牽強,意會就行了。
按照上面設計就會造成一個結果,如果消息場景中聯系人的信息比較簡單,是可以了,但如果比較復雜,然后這些屬性都必須體現在 Message 中,這樣就會造成 Message 實體變的非常冗余,有人看到這,可能會說,為什么要在 Message 實體中去關聯 Contact 對象,直接用 SenderId 和 RecipientId 表示不行嗎?比如 Message 實體中的部分代碼:
public int SenderId { get; set; }
public int RecipientId { get; set; }
這樣設計我覺得沒什么不可以,更加簡化了消息場景中的復雜度,直接一個 Message 實體就可以了,操作或存儲起來也很方便,比如我要獲取一個消息進行展示,這個操作可能會在倉儲中進行完成的,獲取 Message 對象后,還要在應用層進行“組裝”DTO,因為消息聯系人展示肯定要用名稱,而不是標識 Id,聽起來似乎很合理。但上面曾說過的聯系人驗證,這個該怎么實現呢?關于這個實現,現在的操作是放在應用層中,因為沒有聯系人對象的說法,它現在表現出來的只是一個 Id 值,而且發送是根據顯示名稱發送的,在發送消息操作中,先根據名稱獲取 Id,然后再根據 Id 獲取 IsGagged,然后才是發送操作,這部分的實現現在和領域沒有半毛錢關系,那它是什么?應用層控制的是工作流程,但顯然這部分工作并不是工作流程,它應該是消息場景中業務的一部分。
還有就是,這部分設計最直白的問題是,首先看上去就有點“不合理”,難道以后應用中對象之間的關聯都必須使用 Id?那這樣的話,也就沒有了“對象關聯”的概念存在了。
再次回到標題上來,聯系人設計為值對象會怎么?在現有的場景中,我覺得沒有什么問題,但針對聯系人的驗證或管理變的復雜的話,這時候就要考慮下,聯系人設計為值對象是否合理,因為現有針對這部分的實現都不是在領域中完成的,為什么不放在領域中?因為現有設計中,聯系人沒有對象的概念,它只是一個值,一個具體的值。所以在領域模型演化的過程中,針對不斷變化的業務場景,根據現有的設計,還需要考慮模型的合理性。
之前列出了實體的兩個特征,下面列一下值對象的幾個特征,來自《實現領域驅動設計》第六章值對象:
- 它度量或者描述了領域中的一件東西。
- 它可以作為不變量。
- 它將不同的相關的屬性組合成一個概念整體(Conceptual Whole)。
- 當度量和描述改變時,可以用另一個值對象予以替換。
- 它可以和其它值對象進行相等性比較。
- 它不會對協作對象造成副作用。
當在應用設計過程中,如果不能準確的區分實體和值對象,可以不妨把應用程序所抽離出來的對象,往實體和值對象的幾個特征上面套,看看哪一個是否更加合理,但設計不是絕對的,一種思想就會導致一種設計,思想的稍微不同,最后的設計可能就會千差萬別。
其實寫到這,就會發現這篇博文的主題并不只是來確定:消息場景中,發件人、收件人是實體?還是值對象?只不過通過這個問題,可以去發現實體和值對象的一些不常遇到的地方,這些東西在以后的設計中可能會有所幫助。當然對于這個問題,你問我是設計為實體?還是值對象?我個人還是比較偏向于實體,嘿嘿。
通過問題探討去學習領域驅動設計,這種方式會一直持續下去,這篇博文就寫到這!
文章列表