1.引言
提到值對象锨侯,我們可能立馬就想到值類型和引用類型嫩海。而在C#中,值類型的代表是strut和enum囚痴,引用類型的代表是class出革、interface、delegate等渡讼。值類型和引用類型的區(qū)別骂束,大家肯定都知道,值類型分配在棧上成箫,引用類型分配在堆上展箱。
那是不是值類型對應的就是值對象,引用類型對應的就是實體嗎蹬昌?很抱歉混驰,不是的。
值對象我們要分開來看皂贩,其包含兩個詞:值和對象栖榨。值是什么?比如明刷,數(shù)字(1婴栽、2、3.14)辈末,字符串(“hello world”愚争、“DDD”),金額(¥50挤聘、$50)轰枝,地址(深圳市南山區(qū)科技園)它們都是一個值,這個值有什么特點呢组去,固定不變鞍陨,表述一個具體的概念。對象又是什么从隆?一切皆為對象诚撵,是對現(xiàn)實世界的抽象缭裆,用來描述一個具體的事物。那值對象=值+對象=將一個值用對象的方式進行表述砾脑,來表達一個具體的固定不變的概念幼驶。
所以了解值對象艾杏,我們關鍵要抓住關鍵字——值韧衣。
2.值的特征
1就是代表數(shù)字1,“Hello DDD”就是一個固定字符串购桑,“¥50”就是表示人民幣50元畅铭。假設你手上有一沓鈔票,我們去超市購物的時候勃蜘,很顯然我們會根據(jù)面額去付款硕噩,不會拿20元當50元花,也不會把美元當人民幣花缭贡,畢竟¥50≠$50炉擅。那對于鈔票來說,我們怎么識別它們阳惹,無非就是鈔票上印刷的數(shù)字面額和貨幣單位谍失。你可能會說了,每張鈔票上都印有編號莹汤,就算同樣面額的毛爺爺快鱼,那它也不一樣。這個陳述纲岭,我竟然無言以對抹竹。但我只想問你,你平時購物付款止潮,是用編號識別面額的扒耘小?編號顯然是銀行關心的事喇闸,與我們無關兢孝。
我們這里提到的數(shù)字面額、貨幣單位和編號仅偎,除此之外還有發(fā)行日期跨蟹,其實都是鈔票的基本特征,在coding中我們會根據(jù)場景選擇性的對某些特征以屬性的形式加以抽象橘沥。而在我們日常消費的場景下窗轩,顯然編號和發(fā)行日期這兩個特征我們可以直接忽略不計。
從上面這個例子我們可用總結出值的特征:
- 表示一個具體的概念
- 通過值的屬性對其識別
- 屬性判等
- 固定不變
3.案例分析
購物網(wǎng)站都會維護客戶收貨地址信息來進行發(fā)貨處理座咆,一個地址信息一般主要包含省份痢艺、城市仓洼、區(qū)縣、街道堤舒、郵政編碼信息色建。
如果要讓我們設計,我們肯定噼里啪啦就把代碼寫下來了:
/// <summary>
/// 地址
/// </summary>
public class Address {
/// <summary>
///Id
/// </summary>
public int AddressId{ get; private set; }
/// <summary>
/// 省份
/// </summary>
public string Province { get; private set; }
/// <summary>
/// 城市
/// </summary>
public string City { get; private set; }
/// <summary>
/// 區(qū)縣
/// </summary>
public string County { get; private set; }
/// <summary>
/// 街道
/// </summary>
public string Street { get; private set; }
/// <summary>
/// 郵政編碼
/// </summary>
public string Zip { get; private set; }
}
}
很簡單的類舌缤,我想你在沒了解DDD值對像之前肯定會這樣寫箕戳,這并不奇怪,我之前也是這樣設計的国撵,為了將Address映射到數(shù)據(jù)庫陵吸,我們需要定義一個AddressId作為主鍵映射,這是數(shù)據(jù)建模的結果介牙。那在DDD中應該如何設計壮虫?別急,我們一步一步的分析环础。
首先囚似,我們要問自己一個問題,地址是什么线得?廣東省深圳市南山區(qū)高新科技園中區(qū)一路 郵政編碼: 518057(騰訊大廈)饶唤,它就是一個標準的地址,表述的是一個具體的不變的位置信息框都。它不會隨著時間而變化搬素,它包含了地址所需要的完整屬性(省份、城市魏保、區(qū)縣熬尺、街道、郵政編碼)信息谓罗。所以粱哼,地址是一個值。
按照我們現(xiàn)在的設計檩咱,如果有多個所處騰訊大廈的注冊用戶揭措,我們數(shù)據(jù)庫將存在多條相同的地址信息(只是Id不同)。但Id不同刻蚯,就不是同一個地址嗎绊含?我們在做發(fā)貨處理的時候,難道會因為Id不同炊汹,而將貨物發(fā)往不同的地方嗎躬充?很顯然不是的。這也再次論證了地址是一個值的事實。
那我們如何抽象設計這個地址呢充甚,讓其具有值的特征以政?
我們一條一條的來進行分析。
- 表示一個具體的概念
我們上面設計的Address類伴找,也能表示出地址這個概念盈蛮。 - 通過值的屬性對其識別
也就是不需要唯一標識,刪去我們設計的AddressId即可技矮。 - 屬性判等
重寫Equals方法抖誉,比較屬性判斷。 - 固定不變
就是通過構造函數(shù)來初始化穆役,所有屬性均不提供修改入口寸五。
修改后的Address如下:
/// <summary>
/// 地址
/// </summary>
public class Address
{
/// <summary>
/// 省份
/// </summary>
public string Province { get; private set; }
/// <summary>
/// 城市
/// </summary>
public string City { get; private set; }
/// <summary>
/// 區(qū)縣
/// </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("廣東省", "深圳市", "南山區(qū)", "高新科技園中區(qū)一路 ", "518057");)
來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中韧拒,我們稱這個Address
為值對象淹接。讀到這里,你可能會覺得值對象也不過如此叛溢,也可能會有一堆問題塑悼,但請稍安勿躁,我們繼續(xù)講解楷掉。
4.DDD中的值對象
通過上面對值的特征分析厢蒜,結合實際的案例,我們設計出了一個Address
這個值對象烹植。那在DDD中對值對象又是怎樣描述的呢斑鸦?
4.1.值對象的特征
咱們來看看《實現(xiàn)領域驅動設計》上是如何定義的吧:
- 描述了領域中的一件東西
- 不可變的
- 將不同的相關屬性組合成了一個概念整體
- 當度量和描述改變時,可以用另外一個值對象予以替換
- 可以和其他值對象進行相等性比較
- 不會對協(xié)作對象造成副作用
由此可見草雕,值對象包含了值所具有的全部特征巷屿。
另外有一點:個人認為值對象不會孤立的存在,它有其所屬墩虹。比如我們所說的地址嘱巾,它是一個客觀存在。沒有一個具體的上下文語境诫钓,它就僅僅是一個字符串旬昭。只有在某個具體的領域下,才有其實質意義菌湃,比如客戶收貨地址问拘、售后地址。
4.2.值對象的問題
說到問題,你可能想到的第一個問題就是持久化的問題场梆。是的墅冷,值對象沒有標識列如何存儲數(shù)據(jù)庫呢?
當下比較流行使用ORM持久化機制或油,使用ORM將每個類映射到一張數(shù)據(jù)庫表寞忿,再將每個屬性映射到數(shù)據(jù)庫表中的列會增加程序的復雜性。那如何使用ORM持久化來避免這一問題呢顶岸?
- 單個值對象
上面我們提到值對象不會孤立存在腔彰,所以我們可以將值對象中的屬性作為所屬實體/聚合根的數(shù)據(jù)列來存儲(比如,我們可以將收貨地址的屬性映射到客戶實體中)辖佣。這樣做就會導致數(shù)據(jù)表列數(shù)增多霹抛,但是能夠優(yōu)化查詢性能,因為不需要聯(lián)表查詢卷谈。 - 多個值對像序列化到單個列
當每個客戶僅允許維護一個收貨地址時杯拐,我們用上面的方式?jīng)]有問題。但很顯然一個客戶可以有多個收貨地址世蔗。這個時候我們該怎么持久化值對象集合呢端逼?不可能把值對象集合的每個元素映射到外層的實體表中,但是創(chuàng)建多個表又增加復雜性污淋,所以一個變態(tài)的方法是使用序列化大對象模式顶滩。把一個集合序列化后塞到外層實體表的某一列中,是有點匪夷所思寸爆。而且數(shù)據(jù)庫的列寬是有限制的礁鲁,且不方便查詢。但似乎也帶來一個好處赁豆,大大簡化了系統(tǒng)的設計(不用設計多列分別存儲了)仅醇。 - 使用數(shù)據(jù)庫實體保存多個值對像
使用層超類型來賦予值對象一個委派標識,以數(shù)據(jù)庫實體的形式保存值對象歌憨。(關于層超類型着憨,可參考我上一篇文章,這里不作贅述务嫡。)
你可能會覺得第3個方法好甲抖,因為其更符合傳統(tǒng)的設計方式,但其并非DDD推崇的一種方式心铃,因為層超類型讓值對象有了實體的影子准谚。在進行持久化設計的時候,我們要謹記根據(jù)領域模型來設計數(shù)據(jù)模型去扣,而不是根據(jù)數(shù)據(jù)模型來設計領域模型柱衔。
4.3.值對象的作用
通過上面的分析介紹樊破,我們可以體會到值對象帶來的以下好處:
- 符合通用語言,更簡單明了的表達簡單業(yè)務概念唆铐。
- 提升系統(tǒng)性能哲戚。
- 簡化設計,減少不必要的數(shù)據(jù)庫表設計艾岂。
5.建模值對象
值對象作為領域建模工具之一顺少,有其存在的意義。領域中王浴,并不是每一個事物都必須有一個唯一身份標識脆炎,對于某些對象,我們更關心它是什么而無需關心它是哪個氓辣。所以建模值對象秒裕,我們關鍵要結合通用語言的表述看其是否有值的含義和特征。
6. 總結
如果非要對值對象進行總結的話钞啸,我希望你記住我開頭的那句話:
值對象=值+對象=將一個值用對象的方式進行表述几蜻,來表達一個具體的固定不變的概念。
仔細揣摩爽撒,定有收獲入蛆。
參考資料
應用程序框架實戰(zhàn)十六:DDD分層架構之值對象(介紹篇)
DDD領域驅動設計(二) 之 值對象
值對象的威力