前言
哈嘍维费,老張是周四放松又開始了果元,這些天的工作真的是繁重,三個項目同時啟動掩完,沒辦法噪漾,只能在深夜寫文章了硼砰,現(xiàn)在時間的周四凌晨且蓬,白天上班已經(jīng)沒有時間開始寫文章了,希望看到文章的小伙伴题翰,能給個辛苦贊??哈哈恶阴,當(dāng)然看心情很隨意。廢話不多說豹障,話說上次咱們對DDD簡單說明了下存在的意義冯事,還有就是基于教學(xué)上下文的第一次定義,今天咱們就繼續(xù)說說DDD領(lǐng)域驅(qū)動設(shè)計中的聚合相關(guān)知識血公,聚合這一塊比較多昵仅,我暫時決定用兩到三篇文章來說說,今天就主要說一下“實體和值對象”的相關(guān)概念,其實之前我在定計劃的時候摔笤,感覺這一塊應(yīng)該很好說够滑,但是晚上吃完飯搜索資料的時候,發(fā)現(xiàn)真的好多人對實體理解的還好吕世,但是對值對象真是各種不理解彰触,甚至嗤之以鼻,這一點我感覺是不好的命辖,希望我的讀者不要只會說這個不好况毅,那個不對,而是想尔艇,這個東西既然產(chǎn)生了尔许,并且一直被大家說著,也有在使用的终娃,肯定有存在的意義母债,舉個栗子,可能今天大家看完對值對象還是蒙朧朧尝抖,多想想毡们,多跟著DDD的思想走,也許就好多了昧辽,思想真的很難改變衙熔,不過只要努力了就是成功了。
好搅荞!咱們還是開篇一個小問題红氯,給大家正好一個思考的時間:
咱們從壹大學(xué)的后臺系統(tǒng)中,每個學(xué)生都有自己的家庭住址咕痛,肯定會有這樣或那樣的原因痢甘,會變化,那我們是如何設(shè)計 Student模型 和 Address 模型的呢茉贡,這里只是說代碼實現(xiàn)上塞栅,數(shù)據(jù)庫其實是對應(yīng)的。
1腔丧、在Students實體中放椰,添加家庭地址屬性:省、市愉粤、縣砾医、街道;
2衣厘、新建家庭地址Address實體如蚜,在Student中引入地址外鍵;
3、新建 Students 错邦、Address涎显、StuAdd三個表,在Students中引入List<Address>兴猩,一對多期吓;
這個就是我們平時的思路,無論是第一種的一對一(一個學(xué)生一個家庭地址)倾芝,還是第三種的一對多(一個學(xué)生多個家庭地址)讨勤,如果你對這個思路很熟悉,那就需要好好看看今天的文章了晨另,因為上邊的這種還是面向數(shù)據(jù)庫數(shù)據(jù)開發(fā)的潭千,希望下邊的說明,能讓你對DDD的思想有一定的體驗借尿。
零刨晴、今天要實現(xiàn)藍(lán)色的部分
一、實體 —— 唯一標(biāo)識
實體對應(yīng)的英語單詞為Entity路翻。提到實體狈癞,你可能立馬就想到了代碼中定義的實體類。在使用一些ORM框架時茂契,比如Entity Framework蝶桶,實體作為直接反映數(shù)據(jù)庫表結(jié)構(gòu)的對象,就更尤為重要掉冶。特別是當(dāng)我們使用EF Code First時真竖,我們首先要做的就是實體類的設(shè)計。在DDD中厌小,實體作為領(lǐng)域建模的工具之一恢共,也是十分重要的概念。
但DDD中的實體和我們以往開發(fā)中定義的實體是同一個概念嗎璧亚?
不完全是讨韭。在以往未實施DDD的項目中,我們習(xí)慣于將關(guān)注點放在數(shù)據(jù)上涨岁,而非領(lǐng)域上拐袜。這也就說明了為什么我們在軟件開發(fā)過程中會首先做數(shù)據(jù)庫的設(shè)計,進(jìn)而根據(jù)數(shù)據(jù)庫表結(jié)構(gòu)設(shè)計相應(yīng)的實體對象梢薪,這樣的實體對象是數(shù)據(jù)模型轉(zhuǎn)換的結(jié)果。
在DDD中尝哆,實體作為一個領(lǐng)域概念秉撇,在設(shè)計實體時,我們將從領(lǐng)域出發(fā)。
1琐馆、DDD中的實體是什么
許多對象不是由它們的屬性來定義规阀,而是通過一系列的連續(xù)性(continuity)和標(biāo)識(identity)來從根本上定義的。只要一個對象在生命周期中能夠保持連續(xù)性瘦麸,并且獨立于它的屬性(即使這些屬性對系統(tǒng)用戶非常重要)谁撼,那它就是一個實體。
對于實體Entity滋饲,實體核心是用唯一的標(biāo)識符來定義厉碟,而不是通過屬性來定義。即即使屬性完全相同也可能是兩個不同的對象屠缭。同時實體本身有狀態(tài)的箍鼓,實體又演進(jìn)的生命周期,實體本身會體現(xiàn)出相關(guān)的業(yè)務(wù)行為呵曹,業(yè)務(wù)行為會實體屬性或狀態(tài)造成影響和改變款咖。
如果從值對象本身無狀態(tài),不可變奄喂,并且不分配具體的標(biāo)識層面來看铐殃。那么值對象可以僅僅理解為實際的Entity對象的一個屬性結(jié)合而已。該值對象附屬在一個實際的實體對象上面跨新。值對象本身不存在一個獨立的生命周期背稼,也一般不會產(chǎn)生獨立的行為跟衅。
2寇壳、為什么要使用實體
當(dāng)我們需要考慮一個對象的個性特征凿试,或者要區(qū)分不同對象的時候庭猩,我們就需要一個實體這個領(lǐng)域概念厢蒜,一個實體是一個唯一的東西丸冕,并且可以長時間相當(dāng)長的一段時間內(nèi)持續(xù)的變化嚎研,但是無論我們做了多少變化筒繁,這個的實體對象可能也已經(jīng)變化的很多了许饿,但是因為他們都一個相同的身份標(biāo)識阳欲,所有還是同一個實體。很簡單陋率,就好像一個學(xué)生球化,無論手機(jī)號,姓名瓦糟,年齡筒愚,郵箱,是否畢業(yè)等等菩浙,全部變化了巢掺,因為唯一標(biāo)識的原因句伶,我們就可以認(rèn)為,變化前后的所有對象陆淀,都是同一個實體考余。隨著對象的改變,我們可能會一直跟蹤變化過程轧苫,什么時候楚堤,什么人,發(fā)生了什么變化:就比如學(xué)生因為學(xué)習(xí)太好含懊,學(xué)校研究通過身冬,提前畢業(yè),更新狀態(tài)為已畢業(yè)等绢要。
這個時候我們發(fā)現(xiàn)了吏恭,實體的兩大特性:
1、有唯一的標(biāo)識重罪,不受狀態(tài)屬性的影響樱哼。
2、可變性特征剿配,狀態(tài)信息一直可以變化搅幅。
二、定義一個實體
在我們之前的代碼中呼胚,我們定義了 Student 模型茄唐,我們是在當(dāng)前模型中,添加了唯一標(biāo)識
public class Student
{
protected Student() { }
public Student(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
}
public Guid Id { get; private set; }//模型的唯一標(biāo)識
public string Name { get; private set; }
public string Email { get; private set; }
public string Phone { get; private set; }
public DateTime BirthDate { get; private set; }
}
我們平時用到的標(biāo)識都是 Int 類型蝇更,優(yōu)點是占位少沪编,內(nèi)存小等,當(dāng)然有時候受到長度的影響年扩,我們就用 long蚁廓,
1、唯一標(biāo)識都是什么類型
一般我們都是會傾向于使用int類型厨幻,映射到數(shù)據(jù)庫中的自增長int相嵌。它的優(yōu)勢是簡單,唯一性由數(shù)據(jù)庫保障况脆,占用空間小饭宾,查詢速度快。我之前也采用了很長時間格了,大部分時候很好用看铆,不過偶爾會很頭痛。由于實體標(biāo)識需要等到插入數(shù)據(jù)庫之后才創(chuàng)建出來笆搓,所以你在保存之前不可能知道標(biāo)識值是多少性湿,如果在保存之前需要拿到Id,唯一的方法是先插入數(shù)據(jù)庫纬傲,得到Id以后满败,再執(zhí)行另外的操作肤频,換句話說,需要把本來是同一個事務(wù)中的操作分成多個事務(wù)執(zhí)行算墨。除了這個問題宵荒,還有多個數(shù)據(jù)庫表合并的問題,如果兩個分表都是自增净嘀,那肯定需要單獨再一個字段來做標(biāo)識报咳,勞民傷財。
后來我就用string字符串來設(shè)置主鍵挖藏,最大的問題就出現(xiàn)了暑刃,就是有時候會出現(xiàn)一致的情況,倒是保存失敗膜眠,然后用戶反饋岩臣,當(dāng)測試的時候,又好了宵膨,這種幽靈事件架谎。所以我就決定使用 Guid 了。
它的主要優(yōu)勢是生成Guid非常容易辟躏,不論是Js,C#還是在數(shù)據(jù)庫中谷扣,都能輕易的生成出來。另外捎琐,Guid的唯一性很強(qiáng)会涎,基本不可能生成出兩個相同的Guid。
Guid類型的主要缺點是占用空間太大瑞凑。另外實體標(biāo)識一般映射到數(shù)據(jù)庫的主鍵末秃,而Sql Server會默認(rèn)把主鍵設(shè)成聚集索引,由于Guid的不連續(xù)性拨黔,這可能導(dǎo)致大量的頁拆分蛔溃,造成大量碎片從而拖慢查詢。一個解決辦法是使用Sql Server來生成Guid篱蝇,它可以生成連續(xù)的Guid值贺待,但這又回到了老路,只有插入數(shù)據(jù)庫你才知道具體的Id值零截,所以行不通麸塞。另一個解決辦法是把聚集索引移到其它列上,比如創(chuàng)建時間涧衙。如果你打算把聚集索引繼續(xù)放到Guid標(biāo)識列上哪工,可以觀察到碎片一般都在90%以上奥此,寫一個Sql腳本,定時在半夜整理一下碎片雁比,也算一個勉強(qiáng)的辦法稚虎。
如果生成一個有意義的流水號來作為標(biāo)識,這時候標(biāo)識類型就是一個字符串偎捎。
有些時候可能還要使用更復(fù)雜的組合標(biāo)識蠢终,這一般需要創(chuàng)建一個值對象作為標(biāo)識類型。
既然每個實體都有一個標(biāo)識茴她,那么為所有實體創(chuàng)建一個基類就顯得很有用了寻拂,這個基類就是層超類型,它為所有領(lǐng)域?qū)嶓w提供基礎(chǔ)服務(wù)丈牢。
2祭钉、創(chuàng)建領(lǐng)域核心類庫,并添加實體
在領(lǐng)域驅(qū)動設(shè)計中己沛,我們會有一些核心的公共的核心內(nèi)容慌核,所以類庫 Christ.Domain.Core 就是起到的這個作用,除了領(lǐng)域模型外泛粹,還有以后的事件遂铡、命令和通知等核心內(nèi)容類。
因為實體屬于領(lǐng)域模型內(nèi)容晶姊,所以我們新建一個 Models 文件夾扒接,并在其新建 Entity.cs 文件
這個時候,如果你問我们衙,為什么要單單定義一個 Entity 基類钾怔,而不把 Id 放到每一個實體中,嗯蒙挑,那就是還沒有命名領(lǐng)域設(shè)計中宗侦,基于業(yè)務(wù)的考慮,我們平時都是直接用面向數(shù)據(jù)庫數(shù)據(jù)的思想來考慮的忆蚀,duang duang設(shè)計表結(jié)構(gòu)矾利,自然而然的想到每一個表(實體模型)必須有一個Id,但是現(xiàn)在馋袜,我們是基于業(yè)務(wù)考慮的男旗,每一個業(yè)務(wù)下邊會有子領(lǐng)域,然后每個子領(lǐng)域都是聚合的欣鳖,通過一個聚合根來關(guān)聯(lián)察皇,把相似的功能或者根單獨拿出來,這個就是實體基類 Entity 的作用泽台,當(dāng)然除了 Id 還會有一些方法什荣,比如以下:
namespace Christ.Domain.Core.Models
{
/// <summary>
/// 定義領(lǐng)域?qū)嶓w基類
/// </summary>
public abstract class Entity
{
/// <summary>
/// 唯一標(biāo)識
/// </summary>
public Guid Id { get; protected set; }
/// <summary>
/// 重寫方法 相等運算
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
var compareTo = obj as Entity;
if (ReferenceEquals(this, compareTo)) return true;
if (ReferenceEquals(null, compareTo)) return false;
return Id.Equals(compareTo.Id);
}
/// <summary>
/// 重寫方法 實體比較 ==
/// </summary>
/// <param name="a">領(lǐng)域?qū)嶓wa</param>
/// <param name="b">領(lǐng)域?qū)嶓wb</param>
/// <returns></returns>
public static bool operator ==(Entity a, Entity b)
{
if (ReferenceEquals(a, null) && ReferenceEquals(b, null))
return true;
if (ReferenceEquals(a, null) || ReferenceEquals(b, null))
return false;
return a.Equals(b);
}
/// <summary>
/// 重寫方法 實體比較 !=
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static bool operator !=(Entity a, Entity b)
{
return !(a == b);
}
/// <summary>
/// 獲取哈希
/// </summary>
/// <returns></returns>
public override int GetHashCode()
{
return (GetType().GetHashCode() * 907) + Id.GetHashCode();
}
/// <summary>
/// 輸出領(lǐng)域?qū)ο蟮臓顟B(tài)
/// </summary>
/// <returns></returns>
public override string ToString()
{
return GetType().Name + " [Id=" + Id + "]";
}
}
}
3矾缓、實體模型繼承該Entity
修改我們的 Student 模型,繼承 Entity稻爬,并把屬性 Id 去掉嗜闻。
這個時候,我們就已經(jīng)把實體說完了因篇,其實很簡單泞辐,我們平時也都在用笔横,總結(jié)來說以下兩點:
1竞滓、實體的2大特性:唯一標(biāo)識、可變性特性吹缔;
2商佑、通過業(yè)務(wù)的思維,去思考為什么定義 Entity 的作用厢塘,主要也是起到了一個聚合的目的茶没。
那實體我們現(xiàn)在已經(jīng)理解了它的概念,作用晚碾,產(chǎn)生以及意義抓半,剩下的還有一個是實體驗證支持,這個以后再說到格嘁,說到了實體笛求,與之對應(yīng)的是值對象,那值對象又是什么呢糕簿?請往下看探入。
三、值對象 —— 不變性
前面介紹了DDD分層架構(gòu)的實體懂诗,并完成了實體層超類型的開發(fā)( 就是Entity )蜂嗽,本篇將介紹另一個重要的構(gòu)造塊——值對象,它是聚合中的主要成分殃恒。在我們之前的開發(fā)中植旧,因為是基于數(shù)據(jù)庫數(shù)據(jù)的,所以我們基本都是通過數(shù)據(jù)表來建立模型离唐,這就是數(shù)據(jù)建模病附,然后依賴的是數(shù)據(jù)庫范式設(shè)計,這樣我們就把每一個數(shù)據(jù)庫表就對應(yīng)一個實體模型侯繁,每一個表字段就對應(yīng)應(yīng)該實體屬性胖喳。
在看我們文章開頭的那個問題,我們就常常用第一種方法贮竟,
public class Student : Entity
{ protected Student() { } public Student(Guid id, string name, string email, DateTime birthDate)
{
Id = id;
Name = name;
Email = email;
BirthDate = birthDate;
} //public Guid Id { get; private set; }
/// <summary>
/// 姓名 /// </summary>
public string Name { get; private set; } /// <summary>
/// 郵箱 /// </summary>
public string Email { get; private set; } /// <summary>
/// 手機(jī) /// </summary>
public string Phone { get; private set; } /// <summary>
/// 生日 /// </summary>
public DateTime BirthDate { 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; }
}
但是丽焊,為了考慮不該有的屬性较剃,比如家庭地址信息,不應(yīng)該出現(xiàn)在學(xué)生student的業(yè)務(wù)模型中技健,我們就拆開写穴,用兩個實體進(jìn)行表示,然后引入外鍵雌贱,就是我們第二種方法啊送。
public class Student : Entity
{ //.....其他屬性
/// <summary>
/// 地址外鍵 /// </summary>
public Address Address { get; private set; }
} /// <summary>
/// 地址 /// </summary>
public class Address :Entity {/// <summary>
/// 省份 /// </summary>
public string Province { get; private set; } /// <summary>
/// 城市 /// </summary>
public string City { get; private set; }
}
}
可以看到,對于這樣的簡單場景欣孤,一般有兩個選擇馋没,要么把屬性放到外部的實體中,只創(chuàng)建一張表降传,要么建立兩個實體篷朵,并相應(yīng)的創(chuàng)建兩張表。第一種方法的缺點是婆排,全部屬性值放到一切声旺,沒有了整體業(yè)務(wù)概念,不僅無法表達(dá)業(yè)務(wù)語義段只,而且使用起來非常困難腮猖,同時將很多不必要的業(yè)務(wù)知識泄露到調(diào)用端。第二種方法的問題是導(dǎo)致了不必要的復(fù)雜性赞枕。
更好的方法很簡單澈缺,就是把以上兩種方法結(jié)合起來。我們通過把地址建模成值對象鹦赎,而不是實體谍椅,然后把值對象的屬性值嵌入外部員工實體的表中,這種映射方式被稱為嵌入值模式古话。換句話說雏吭,你現(xiàn)在的數(shù)據(jù)庫表采用上面的第一種方式定義,而你在c#代碼中通過第二種方式使用陪踩,只是把實體改成值對象杖们。這樣做的好處是顯而易見的,既將業(yè)務(wù)概念表達(dá)得清楚肩狂,而且數(shù)據(jù)庫也沒有變得復(fù)雜摘完。
1、值對象的概念
值對象雖然有時候和實體特別想象傻谁,看上邊的學(xué)校家庭信息就可得知孝治,但是它卻有著自己獨有的好處,值對象很常見:比如數(shù)字,字符串谈飒,日期時間岂座,甚至一個人的信息,郵寄地址等等杭措,當(dāng)然還有更復(fù)雜的值對象费什,這些都是反映 通用語言 概念的值對象。
我們應(yīng)該盡量使用值對象來建模手素,而不是實體對象鸳址,你可能很想不通,即使上邊的學(xué)生的家庭地址信息泉懦,你一定要單放一個數(shù)據(jù)庫表稿黍,構(gòu)建實體模型,在設(shè)計的時候我們應(yīng)該也要更偏向作為一個值對象容器祠斧,而不是子實體容器闻察,因為這樣我們可以對值對象很好的創(chuàng)建,測試琢锋,使用,優(yōu)化和維護(hù)呢灶。
當(dāng)你決定一個領(lǐng)域概念是否是一個值對象的時候吴超,你需要考慮它是否有以下特性:
1、它描述了領(lǐng)域中的一個東西
2鸯乃、可以作為一個不變量鲸阻。
3、當(dāng)它被改變時缨睡,可以用另一個值對象替換鸟悴。
4、可以和別的值對象進(jìn)行相等性比較奖年。
在值對象中细诸,我們不關(guān)心標(biāo)識,只要我們能確定該值對象的屬性值都一樣陋守,我們就可以說這兩個值對象是相同的震贵,比如我們說兩個學(xué)生的家庭地址(省市縣街道門排)是一樣的,我們就可以認(rèn)為是同一個地址水评,這就是相等性比較猩系。
如果學(xué)生在修改地址的時候,我們不是僅僅的修改省中燥,或者市寇甸,或者縣,而且將整個值對象給覆蓋,這個就是值對象的不變性和可替換性拿霉。
四式塌、如何創(chuàng)建一個地址值對象
1、創(chuàng)建值對象基類
在 Christ3D.Domain.Core 類庫下的Models文件夾中友浸,新建 ValueObject.cs
namespace Christ3D.Domain.Core.Models
{ /// <summary>
/// 定義值對象基類 /// 注意沒有唯一標(biāo)識了 /// </summary>
/// <typeparam name="T"></typeparam>
public abstract class ValueObject<T> where T : ValueObject<T> { /// <summary>
/// 重寫方法 相等運算 /// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{ var valueObject = obj as T; return !ReferenceEquals(valueObject, null) && EqualsCore(valueObject);
} protected abstract bool EqualsCore(T other); /// <summary>
/// 獲取哈希 /// </summary>
/// <returns></returns>
public override int GetHashCode()
{ return GetHashCodeCore();
} protected abstract int GetHashCodeCore(); /// <summary>
/// 重寫方法 實體比較 == /// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static bool operator ==(ValueObject<T> a, ValueObject<T> b)
{ if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true; if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false; return a.Equals(b);
} /// <summary>
/// 重寫方法 實體比較 != /// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
public static bool operator !=(ValueObject<T> a, ValueObject<T> b)
{ return !(a == b);
} /// <summary>
/// 克隆副本 /// </summary>
public virtual T Clone()
{ return (T)MemberwiseClone();
}
}
}
2峰尝、在 Christ3D.Domain 類庫下的Models文件夾中,新建 Address 值對象
namespace Christ3D.Domain.Models
{ public Address(string province, string city, string county, string street, string zip)
{ this.Province = province; this.City = city; this.County = county; this.Street = street;
} /// <summary>
/// 地址 /// </summary>
public class Address : ValueObject<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; } protected override bool EqualsCore(Address other)
{ throw new NotImplementedException();
} protected override int GetHashCodeCore()
{ throw new NotImplementedException();
}
}
}
至此收恢,我們的Address就具有了值的特征武学,我們可以直接使用Address address = new Address("北京市", "北京市", "海淀區(qū)", "一路 ");)來表示一個具體的通過屬性識別的不可變的位置概念。在DDD中伦意,我們稱這個Address為值對象火窒。
3、實體與值對象的區(qū)別:
- 實體擁有標(biāo)識驮肉,而值對象沒有熏矿。
- 相等性測試方式不同。實體根據(jù)標(biāo)識判等离钝,而值對象根據(jù)內(nèi)部所有屬性值判等票编。
- 實體允許變化,值對象不允許變化卵渴。
- 持久化的映射方式不同慧域。實體采用單表繼承、類表繼承和具體表繼承來映射類層次結(jié)構(gòu)浪读,而值對象使用嵌入值或序列化大對象方式映射昔榴。
五、結(jié)語(待續(xù))
今天因為時間的問題暫時就說這么多吧碘橘,這里只是把 實體 和值對象的概念和使用說明了下互订,具體的好處和強(qiáng)大的優(yōu)勢還沒有來得及說,下一篇文章痘拆,我會說繼續(xù)說聚合的內(nèi)容仰禽,包括實體驗證等,這篇文章也需要慢慢的潤潤色错负,加油吧