1.引言
提到值對象,我們可能立馬就想到值類型和引用類型。而在C#中,值類型的代表是strut和enum,引用類型的代表是class、interface、delegate等。值類型和引用類型的區別,大家肯定都知道,值類型分配在棧上,引用類型分配在堆上。
那是不是值類型對應的就是值對象,引用類型對應的就是實體嗎?很抱歉,不是的。
值對象我們要分開來看,其包含兩個詞:值和對象。值是什么?比如,數字(1、2、3.14),字符串(“hello world”、“DDD”),金額(¥50、$50),地址(深圳市南山區科技園)它們都是一個值,這個值有什么特點呢,固定不變,表述一個具體的概念。對象又是什么?一切皆為對象,是對現實世界的抽象,用來描述一個具體的事物。那值對象=值+對象=將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念。
所以了解值對象,我們關鍵要抓住關鍵字——值。
2.值的特征
1就是代表數字1,“Hello DDD”就是一個固定字符串,“¥50”就是表示人民幣50元。假設你手上有一沓鈔票,我們去超市購物的時候,很顯然我們會根據面額去付款,不會拿20元當50元花,也不會把美元當人民幣花,畢竟¥50≠$50。那對於鈔票來說,我們怎么識別它們,無非就是鈔票上印刷的數字面額和貨幣單位。你可能會說了,每張鈔票上都印有編號,就算同樣面額的毛爺爺,那它也不一樣。這個陳述,我竟然無言以對。但我只想問你,你平時購物付款,是用編號識別面額的啊?編號顯然是銀行關心的事,與我們無關。
我們這里提到的數字面額、貨幣單位和編號,除此之外還有發行日期,其實都是鈔票的基本特征,在coding中我們會根據場景選擇性的對某些特征以屬性的形式加以抽象。而在我們日常消費的場景下,顯然編號和發行日期這兩個特征我們可以直接忽略不計。
從上面這個例子我們可用總結出值的特征:
- 表示一個具體的概念
- 通過值的屬性對其識別
- 屬性判等
- 固定不變
3.案例分析
購物網站都會維護客戶收貨地址信息來進行發貨處理,一個地址信息一般主要包含省份、城市、區縣、街道、郵政編碼信息。
如果要讓我們設計,我們肯定噼里啪啦就把代碼寫下來了:
/// <summary>
/// 地址
/// </summary>
public class Address {
/// <summary>
///Id
/// </summary>
public int AddressId{ get; set; }
/// <summary>
/// 省份
/// </summary>
public string Province { get; set; }
/// <summary>
/// 城市
/// </summary>
public string City { get; set; }
/// <summary>
/// 區縣
/// </summary>
public string County { get; set; }
/// <summary>
/// 街道
/// </summary>
public string Street { get; set; }
/// <summary>
/// 郵政編碼
/// </summary>
public string Zip { get; set; }
}
}
很簡單的類,我想你在沒了解DDD值對像之前肯定會這樣寫,這並不奇怪,我之前也是這樣設計的,為了將Address映射到數據庫,我們需要定義一個AddressId作為主鍵映射,這是數據建模的結果。那在DDD中應該如何設計?別急,我們一步一步的分析。
首先,我們要問自己一個問題,地址是什么?廣東省深圳市南山區高新科技園中區一路 郵政編碼: 518057(騰訊大廈),它就是一個標准的地址,表述的是一個具體的不變的位置信息。它不會隨着時間而變化,它包含了地址所需要的完整屬性(省份、城市、區縣、街道、郵政編碼)信息。所以,地址是一個值。
按照我們現在的設計,如果有多個所處騰訊大廈的注冊用戶,我們數據庫將存在多條相同的地址信息(只是Id不同)。但Id不同,就不是同一個地址嗎?我們在做發貨處理的時候,難道會因為Id不同,而將貨物發往不同的地方嗎?很顯然不是的。這也再次論證了地址是一個值的事實。
那我們如何抽象設計這個地址呢,讓其具有值的特征?
我們一條一條的來進行分析。
- 表示一個具體的概念
我們上面設計的Address類,也能表示出地址這個概念。 - 通過值的屬性對其識別
也就是不需要唯一標識,刪去我們設計的AddressId即可。 - 屬性判等
重寫Equals方法,比較屬性判斷。 - 固定不變
就是通過構造函數來初始化,所有屬性均不提供修改入口。
修改后的Address如下:
/// <summary>
/// 地址
/// </summary>
public class Address
{
/// <summary>
/// 省份
/// </summary>
public string Province { get; private set; }
/// <summary>
/// 城市
/// </summary>
public string City { get; private set; }
/// <summary>
/// 區縣
/// </summary>
public string County { get; private set; }
/// <summary>
/// 街道
/// </summary>
public string Street { get; private set; }
/// <summary>
/// 郵政編碼
/// </summary>
public string Zip { get; private set; }
public Address(string province, string city,
string county, string street, string zip)
{
this.Province = province;
this.City = city;
this.County = county;
this.Street = street;
this.Zip = zip;
}
public override bool Equals(object obj)
{
bool isEqual = false;
if (obj != null && this.GetType() == obj.GetType())
{
var that = obj as Address;
isEqual = this.Province == that.Province
&& this.City == that.City
&& this.County == that.County
&& this.Street == that.Street
&& this.Zip == that.Zip;
}
return isEqual;
}
public override int GetHashCode()
{
return this.ToString().GetHashCode();
}
public override string ToString()
{
string address = $"{this.Province}{this.City}" +
$"{this.County}{this.Street}({this.Zip})";
return address;
}
}
至此,我們的Address
就具有了值的特征,我們可以直接使用Address address = new Address("廣東省", "深圳市", "南山區", "高新科技園中區一路 ", "518057");)
來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中,我們稱這個Address
為值對象。讀到這里,你可能會覺得值對象也不過如此,也可能會有一堆問題,但請稍安勿躁,我們繼續講解。
4.DDD中的值對象
通過上面對值的特征分析,結合實際的案例,我們設計出了一個Address
這個值對象。那在DDD中對值對象又是怎樣描述的呢?
4.1.值對象的特征
咱們來看看《實現領域驅動設計》上是如何定義的吧:
- 描述了領域中的一件東西
- 不可變的
- 將不同的相關屬性組合成了一個概念整體
- 當度量和描述改變時,可以用另外一個值對象予以替換
- 可以和其他值對象進行相等性比較
- 不會對協作對象造成副作用
由此可見,值對象包含了值所具有的全部特征。
另外有一點:個人認為值對象不會孤立的存在,它有其所屬。比如我們所說的地址,它是一個客觀存在。沒有一個具體的上下文語境,它就僅僅是一個字符串。只有在某個具體的領域下,才有其實質意義,比如客戶收貨地址、售后地址。
4.2.值對象的問題
說到問題,你可能想到的第一個問題就是持久化的問題。是的,值對象沒有標識列如何存儲數據庫呢?
當下比較流行使用ORM持久化機制,使用ORM將每個類映射到一張數據庫表,再將每個屬性映射到數據庫表中的列會增加程序的復雜性。那如何使用ORM持久化來避免這一問題呢?
- 單個值對象
上面我們提到值對象不會孤立存在,所以我們可以將值對象中的屬性作為所屬實體/聚合根的數據列來存儲(比如,我們可以將收貨地址的屬性映射到客戶實體中)。這樣做就會導致數據表列數增多,但是能夠優化查詢性能,因為不需要聯表查詢。 - 多個值對像序列化到單個列
當每個客戶僅允許維護一個收貨地址時,我們用上面的方式沒有問題。但很顯然一個客戶可以有多個收貨地址。這個時候我們該怎么持久化值對象集合呢?不可能把值對象集合的每個元素映射到外層的實體表中,但是創建多個表又增加復雜性,所以一個變態的方法是使用序列化大對象模式。把一個集合序列化后塞到外層實體表的某一列中,是有點匪夷所思。而且數據庫的列寬是有限制的,且不方便查詢。但似乎也帶來一個好處,大大簡化了系統的設計(不用設計多列分別存儲了)。 - 使用數據庫實體保存多個值對像
使用層超類型來賦予值對象一個委派標識,以數據庫實體的形式保存值對象。(關於層超類型,可參考我上一篇文章,這里不作贅述。)
你可能會覺得第3個方法好,因為其更符合傳統的設計方式,但其並非DDD推崇的一種方式,因為層超類型讓值對象有了實體的影子。在進行持久化設計的時候,我們要謹記根據領域模型來設計數據模型,而不是根據數據模型來設計領域模型。
4.3.值對象的作用
通過上面的分析介紹,我們可以體會到值對象帶來的以下好處:
- 符合通用語言,更簡單明了的表達簡單業務概念。
- 提升系統性能。
- 簡化設計,減少不必要的數據庫表設計。
5.建模值對象
值對象作為領域建模工具之一,有其存在的意義。領域中,並不是每一個事物都必須有一個唯一身份標識,對於某些對象,我們更關心它是什么而無需關心它是哪個。所以建模值對象,我們關鍵要結合通用語言的表述看其是否有值的含義和特征。
6. 總結
如果非要對值對象進行總結的話,我希望你記住我開頭的那句話:
值對象=值+對象=將一個值用對象的方式進行表述,來表達一個具體的固定不變的概念。
仔細揣摩,定有收獲。