哈嘍,老張是週四放鬆又開始了,這些天的工做真的是繁重,三個項目同時啓動,沒辦法,只能在深夜寫文章了,如今時間的週四凌晨,白天上班已經沒有時間開始寫文章了,但願看到文章的小夥伴,能給個辛苦贊👍哈哈,固然看心情很隨意。廢話很少說,話說上次我們對DDD簡單說明了下存在的意義,還有就是基於教學上下文的第一次定義,今天我們就繼續說說DDD領域驅動設計中的聚合相關知識,聚合這一塊比較多,我暫時決定用兩到三篇文章來講說,今天就主要說一下「實體和值對象」的相關概念,其實以前我在定計劃的時候,感受這一塊應該很好說,可是晚上吃完飯搜索資料的時候,發現真的好多人對實體理解的還好,可是對值對象真是各類不理解,甚至嗤之以鼻,這一點我感受是很差的,但願個人讀者不要只會說這個很差,那個不對,而是想,這個東西既然產生了,而且一直被你們說着,也有在使用的,確定有存在的意義,舉個栗子,可能今天你們看完對值對象仍是蒙朧朧,多想一想,多跟着DDD的思想走,也許就好多了,思想真的很難改變,不過只要努力了就是成功了。git
好!我們仍是開篇一個小問題,給你們正好一個思考的時間:github
我們從壹大學的後臺系統中,每一個學生都有本身的家庭住址,確定會有這樣或那樣的緣由,會變化,那咱們是如何設計 Student模型 和 Address 模型的呢,這裏只是說代碼實現上,數據庫實際上是對應的。數據庫
一、在Students實體中,添加家庭地址屬性:省、市、縣、街道;c#
二、新建家庭地址Address實體,在Student中引入地址外鍵;架構
三、新建 Students 、Address、StuAdd三個表,在Students中引入List<Address>,一對多;框架
這個就是咱們平時的思路,不管是第一種的一對一(一個學生一個家庭地址),仍是第三種的一對多(一個學生多個家庭地址),若是你對這個思路很熟悉,那就須要好好看看今天的文章了,由於上邊的這種仍是面向數據庫數據開發的,但願下邊的說明,能讓你對DDD的思想有必定的體驗。ide
實體對應的英語單詞爲Entity。提到實體,你可能立馬就想到了代碼中定義的實體類。在使用一些ORM框架時,好比Entity Framework,實體做爲直接反映數據庫表結構的對象,就更尤其重要。特別是當咱們使用EF Code First時,咱們首先要作的就是實體類的設計。在DDD中,實體做爲領域建模的工具之一,也是十分重要的概念。工具
但DDD中的實體和咱們以往開發中定義的實體是同一個概念嗎?
不徹底是。在以往未實施DDD的項目中,咱們習慣於將關注點放在數據上,而非領域上。這也就說明了爲何咱們在軟件開發過程當中會首先作數據庫的設計,進而根據數據庫表結構設計相應的實體對象,這樣的實體對象是數據模型轉換的結果。
在DDD中,實體做爲一個領域概念,在設計實體時,咱們將從領域出發。學習
對於實體Entity,實體核心是用惟一的標識符來定義,而不是經過屬性來定義。即即便屬性徹底相同也多是兩個不一樣的對象。同時實體自己有狀態的,實體又演進的生命週期,實體自己會體現出相關的業務行爲,業務行爲會實體屬性或狀態形成影響和改變。測試
若是從值對象自己無狀態,不可變,而且不分配具體的標識層面來看。那麼值對象能夠僅僅理解爲實際的Entity對象的一個屬性結合而已。該值對象附屬在一個實際的實體對象上面。值對象自己不存在一個獨立的生命週期,也通常不會產生獨立的行爲。
一、有惟一的標識,不受狀態屬性的影響。二、可變性特徵,狀態信息一直能夠變化。
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; }//模型的惟一標識 public string Name { get; private set; } public string Email { get; private set; } public string Phone { get; private set; } public DateTime BirthDate { get; private set; } }
咱們平時用到的標識都是 Int 類型,優勢是佔位少,內存小等,固然有時候受到長度的影響,咱們就用 long,
通常咱們都是會傾向於使用int類型,映射到數據庫中的自增加int。它的優點是簡單,惟一性由數據庫保障,佔用空間小,查詢速度快。我以前也採用了很長時間,大部分時候很好用,不過偶爾會很頭痛。因爲實體標識須要等到插入數據庫以後才建立出來,因此你在保存以前不可能知道標識值是多少,若是在保存以前須要拿到Id,惟一的方法是先插入數據庫,獲得Id之後,再執行另外的操做,換句話說,須要把原本是同一個事務中的操做分紅多個事務執行。除了這個問題,還有多個數據庫表合併的問題,若是兩個分表都是自增,那確定須要單獨再一個字段來作標識,勞民傷財。
後來我就用string字符串來設置主鍵,最大的問題就出現了,就是有時候會出現一致的狀況,卻是保存失敗,而後用戶反饋,當測試的時候,又好了,這種幽靈事件。因此我就決定使用 Guid 了。
它的主要優點是生成Guid很是容易,不管是Js,C#仍是在數據庫中,都能輕易的生成出來。另外,Guid的惟一性很強,基本不可能生成出兩個相同的Guid。
Guid類型的主要缺點是佔用空間太大。另外實體標識通常映射到數據庫的主鍵,而Sql Server會默認把主鍵設成彙集索引,因爲Guid的不連續性,這可能致使大量的頁拆分,形成大量碎片從而拖慢查詢。一個解決辦法是使用Sql Server來生成Guid,它能夠生成連續的Guid值,但這又回到了老路,只有插入數據庫你才知道具體的Id值,因此行不通。另外一個解決辦法是把彙集索引移到其它列上,好比建立時間。若是你打算把彙集索引繼續放到Guid標識列上,能夠觀察到碎片通常都在90%以上,寫一個Sql腳本,定時在半夜整理一下碎片,也算一個勉強的辦法。
若是生成一個有意義的流水號來做爲標識,這時候標識類型就是一個字符串。
有些時候可能還要使用更復雜的組合標識,這通常須要建立一個值對象做爲標識類型。
既然每一個實體都有一個標識,那麼爲全部實體建立一個基類就顯得頗有用了,這個基類就是層超類型,它爲全部領域實體提供基礎服務。
namespace Christ.Domain.Core.Models { /// <summary> /// 定義領域實體基類 /// </summary> public abstract class Entity { /// <summary> /// 惟一標識 /// </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">領域實體a</param> /// <param name="b">領域實體b</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> /// 輸出領域對象的狀態 /// </summary> /// <returns></returns> public override string ToString() { return GetType().Name + " [Id=" + Id + "]"; } } }
一、實體的2大特性:惟一標識、可變性特性;二、經過業務的思惟,去思考爲何定義 Entity 的做用,主要也是起到了一個聚合的目的。
前面介紹了DDD分層架構的實體,並完成了實體層超類型的開發( 就是Entity ),本篇將介紹另外一個重要的構造塊——值對象,它是聚合中的主要成分。在咱們以前的開發中,由於是基於數據庫數據的,因此咱們基本都是經過數據表來創建模型,這就是數據建模,而後依賴的是數據庫範式設計,這樣咱們就把每個數據庫表就對應一個實體模型,每個表字段就對應應該實體屬性。
在看咱們文章開頭的那個問題,咱們就經常用第一種方法,
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> /// 手機 /// </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> /// 區縣 /// </summary> public string County { get; private set; } /// <summary> /// 街道 /// </summary> public string Street { get; private set; } }
可是,爲了考慮不應有的屬性,好比家庭地址信息,不該該出如今學生student的業務模型中,咱們就拆開,用兩個實體進行表示,而後引入外鍵,就是咱們第二種方法。
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; } } }
能夠看到,對於這樣的簡單場景,通常有兩個選擇,要麼把屬性放到外部的實體中,只建立一張表,要麼創建兩個實體,並相應的建立兩張表。第一種方法的缺點是,所有屬性值放到一塊兒,沒有了總體業務概念,不只沒法表達業務語義,並且使用起來很是困難,同時將不少沒必要要的業務知識泄露到調用端。第二種方法的問題是致使了沒必要要的複雜性。
更好的方法很簡單,就是把以上兩種方法結合起來。咱們經過把地址建模成值對象,而不是實體,而後把值對象的屬性值嵌入外部員工實體的表中,這種映射方式被稱爲嵌入值模式。換句話說,你如今的數據庫表採用上面的第一種方式定義,而你在c#代碼中經過第二種方式使用,只是把實體改爲值對象。這樣作的好處是顯而易見的,既將業務概念表達得清楚,並且數據庫也沒有變得複雜。
一、它描述了領域中的一個東西二、能夠做爲一個不變量。三、當它被改變時,能夠用另外一個值對象替換。四、能夠和別的值對象進行相等性比較。
namespace Christ3D.Domain.Core.Models { /// <summary> /// 定義值對象基類 /// 注意沒有惟一標識了 /// </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(); } } }
namespace Christ3D.Domain.Models { /// <summary> /// 地址 /// </summary> [Owned] public class Address : ValueObject<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; } public Address() { } public Address(string province, string city, string county, string street) { this.Province = province; this.City = city; this.County = county; this.Street = street; } protected override bool EqualsCore(Address other) { throw new NotImplementedException(); } protected override int GetHashCodeCore() { throw new NotImplementedException(); } } }
至此,咱們的Address就具備了值的特徵,咱們能夠直接使用Address address = new Address("北京市", "北京市", "海淀區", "一路 ");)來表示一個具體的經過屬性識別的不可變的位置概念。在DDD中,咱們稱這個Address爲值對象。