從壹開始微服務 [ DDD ] 之五 ║聚合:實體與值對象 (上)

前言

哈嘍,老張是週四放鬆又開始了,這些天的工做真的是繁重,三個項目同時啓動,沒辦法,只能在深夜寫文章了,如今時間的週四凌晨,白天上班已經沒有時間開始寫文章了,但願看到文章的小夥伴,能給個辛苦贊👍哈哈,固然看心情很隨意。廢話很少說,話說上次我們對DDD簡單說明了下存在的意義,還有就是基於教學上下文的第一次定義,今天我們就繼續說說DDD領域驅動設計中的聚合相關知識,聚合這一塊比較多,我暫時決定用兩到三篇文章來講說,今天就主要說一下「實體和值對象」的相關概念,其實以前我在定計劃的時候,感受這一塊應該很好說,可是晚上吃完飯搜索資料的時候,發現真的好多人對實體理解的還好,可是對值對象真是各類不理解,甚至嗤之以鼻,這一點我感受是很差的,但願個人讀者不要只會說這個很差,那個不對,而是想,這個東西既然產生了,而且一直被你們說着,也有在使用的,確定有存在的意義,舉個栗子,可能今天你們看完對值對象仍是蒙朧朧,多想一想,多跟着DDD的思想走,也許就好多了,思想真的很難改變,不過只要努力了就是成功了。git

好!我們仍是開篇一個小問題,給你們正好一個思考的時間:github

我們從壹大學的後臺系統中,每一個學生都有本身的家庭住址,確定會有這樣或那樣的緣由,會變化,那咱們是如何設計 Student模型 和 Address 模型的呢,這裏只是說代碼實現上,數據庫實際上是對應的。數據庫

一、在Students實體中,添加家庭地址屬性:省、市、縣、街道;c#

二、新建家庭地址Address實體,在Student中引入地址外鍵;架構

三、新建 Students 、Address、StuAdd三個表,在Students中引入List<Address>,一對多;框架

這個就是咱們平時的思路,不管是第一種的一對一(一個學生一個家庭地址),仍是第三種的一對多(一個學生多個家庭地址),若是你對這個思路很熟悉,那就須要好好看看今天的文章了,由於上邊的這種仍是面向數據庫數據開發的,但願下邊的說明,能讓你對DDD的思想有必定的體驗。ide

 

零、今天要實現藍色的部分

 

 1、實體 —— 惟一標識

實體對應的英語單詞爲Entity。提到實體,你可能立馬就想到了代碼中定義的實體類。在使用一些ORM框架時,好比Entity Framework,實體做爲直接反映數據庫表結構的對象,就更尤其重要。特別是當咱們使用EF Code First時,咱們首先要作的就是實體類的設計。在DDD中,實體做爲領域建模的工具之一,也是十分重要的概念。工具

但DDD中的實體和咱們以往開發中定義的實體是同一個概念嗎?
不徹底是。在以往未實施DDD的項目中,咱們習慣於將關注點放在數據上,而非領域上。這也就說明了爲何咱們在軟件開發過程當中會首先作數據庫的設計,進而根據數據庫表結構設計相應的實體對象,這樣的實體對象是數據模型轉換的結果。
在DDD中,實體做爲一個領域概念,在設計實體時,咱們將從領域出發。學習

一、DDD中的實體是什麼

許多對象不是由它們的屬性來定義,而是經過一系列的連續性(continuity)和標識(identity)來從根本上定義的。只要一個對象在生命週期中可以保持連續性,而且獨立於它的屬性(即便這些屬性對系統用戶很是重要),那它就是一個實體。

對於實體Entity,實體核心是用惟一的標識符來定義,而不是經過屬性來定義。即即便屬性徹底相同也多是兩個不一樣的對象。同時實體自己有狀態的,實體又演進的生命週期,實體自己會體現出相關的業務行爲,業務行爲會實體屬性或狀態形成影響和改變。測試

若是從值對象自己無狀態,不可變,而且不分配具體的標識層面來看。那麼值對象能夠僅僅理解爲實際的Entity對象的一個屬性結合而已。該值對象附屬在一個實際的實體對象上面。值對象自己不存在一個獨立的生命週期,也通常不會產生獨立的行爲。

二、爲何要使用實體

當咱們須要考慮一個對象的個性特徵,或者要區分不一樣對象的時候,咱們就須要一個實體這個領域概念,一個實體是一個惟一的東西,而且能夠長時間至關長的一段時間內持續的變化,可是不管咱們作了多少變化,這個的實體對象可能也已經變化的不少了,可是由於他們都一個相同的身份標識,全部仍是同一個實體。很簡單,就好像一個學生,不管手機號,姓名,年齡,郵箱,是否畢業等等,所有變化了,由於惟一標識的緣由,咱們就能夠認爲,變化先後的全部對象,都是同一個實體。隨着對象的改變,咱們可能會一直跟蹤變化過程,何時,什麼人,發生了什麼變化:就好比學生由於學習太好,學校研究經過,提早畢業,更新狀態爲已畢業等。
這個時候咱們發現了,實體的兩大特性:
一、有惟一的標識,不受狀態屬性的影響。
二、可變性特徵,狀態信息一直能夠變化。
 

2、定義一個實體

 在咱們以前的代碼中,咱們定義了 Student 模型,咱們是在當前模型中,添加了惟一標識 
  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腳本,定時在半夜整理一下碎片,也算一個勉強的辦法。

  若是生成一個有意義的流水號來做爲標識,這時候標識類型就是一個字符串。

  有些時候可能還要使用更復雜的組合標識,這通常須要建立一個值對象做爲標識類型。

既然每一個實體都有一個標識,那麼爲全部實體建立一個基類就顯得頗有用了,這個基類就是層超類型,它爲全部領域實體提供基礎服務。

 

二、建立領域核心類庫,並添加實體

在領域驅動設計中,咱們會有一些核心的公共的核心內容,因此類庫 Christ.Domain.Core 就是起到的這個做用,除了領域模型外,還有之後的事件、命令和通知等核心內容類。
由於實體屬於領域模型內容,因此咱們新建一個 Models 文件夾,並在其新建 Entity.cs 文件
這個時候,若是你問我,爲何要單單定義一個 Entity 基類,而不把 Id 放到每個實體中,嗯,那就是尚未命名領域設計中,基於業務的考慮,咱們平時都是直接用面向數據庫數據的思想來考慮的,duang duang設計表結構,天然而然的想到每個表(實體模型)必須有一個Id,可是如今,咱們是基於業務考慮的,每個業務下邊會有子領域,而後每一個子領域都是聚合的,經過一個聚合根來關聯,把類似的功能或者根單獨拿出來,這個就是實體基類 Entity 的做用,固然除了 Id 還會有一些方法,好比如下:
 
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 + "]";
        }
       
    }
}

 

 

 

三、實體模型繼承該Entity

 修改咱們的 Student 模型,繼承 Entity,並把屬性 Id 去掉。

 

這個時候,咱們就已經把實體說完了,其實很簡單,咱們平時也都在用,總結來講如下兩點:
一、實體的2大特性:惟一標識、可變性特性;
二、經過業務的思惟,去思考爲何定義 Entity 的做用,主要也是起到了一個聚合的目的。
那實體咱們如今已經理解了它的概念,做用,產生以及意義,剩下的還有一個是實體驗證支持,這個之後再說到,說到了實體,與之對應的是值對象,那值對象又是什麼呢?請往下看。
 

3、值對象 —— 不變性

前面介紹了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#代碼中經過第二種方式使用,只是把實體改爲值對象。這樣作的好處是顯而易見的,既將業務概念表達得清楚,並且數據庫也沒有變得複雜。

一、值對象的概念

 值對象雖然有時候和實體特別想象,看上邊的學校家庭信息就可得知,可是它卻有着本身獨有的好處,值對象很常見:好比數字,字符串,日期時間,甚至一我的的信息,郵寄地址等等,固然還有更復雜的值對象,這些都是反映 通用語言 概念的值對象。
咱們應該儘可能使用值對象來建模,而不是實體對象,你可能很想不通,即便上邊的學生的家庭地址信息,你必定要單放一個數據庫表,構建實體模型,在設計的時候咱們應該也要更偏向做爲一個值對象容器,而不是子實體容器,由於這樣咱們能夠對值對象很好的建立,測試,使用,優化和維護。
 
當你決定一個領域概念是不是一個值對象的時候,你須要考慮它是否有如下特性:
一、它描述了領域中的一個東西
二、能夠做爲一個不變量。
三、當它被改變時,能夠用另外一個值對象替換。
四、能夠和別的值對象進行相等性比較。
 
在值對象中,咱們不關心標識,只要咱們能肯定該值對象的屬性值都同樣,咱們就能夠說這兩個值對象是相同的,好比咱們說兩個學生的家庭地址(省市縣街道門排)是同樣的,咱們就能夠認爲是同一個地址,這就是相等性比較。
若是學生在修改地址的時候,咱們不是僅僅的修改省,或者市,或者縣,並且將整個值對象給覆蓋,這個就是值對象的不變性和可替換性。
 

4、如何建立一個地址值對象

一、建立值對象基類

在 Christ3D.Domain.Core 類庫下的Models文件夾中,新建 ValueObject.cs
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();
        }
    }
}

 

二、在 Christ3D.Domain 類庫下的Models文件夾中,新建 Address 值對象

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爲值對象。

 

三、實體與值對象的區別:

  1. 實體擁有標識,而值對象沒有。
  2. 相等性測試方式不一樣。實體根據標識判等,而值對象根據內部全部屬性值判等。
  3. 實體容許變化,值對象不容許變化。
  4. 持久化的映射方式不一樣。實體採用單表繼承、類表繼承和具體表繼承來映射類層次結構,而值對象使用嵌入值或序列化大對象方式映射。
 

5、結語(待續)

今天由於時間的問題暫時就說這麼多吧,這裏只是把 實體 和值對象的概念和使用說明了下,具體的好處和強大的優點尚未來得及說,下一篇文章,我會說繼續說聚合的內容,包括實體驗證等,這篇文章也須要慢慢的潤潤色,加油吧
 

6、Github & Gitee

https://github.com/anjoy8/ChristDDD

https://gitee.com/laozhangIsPhi/ChristDDD 

相關文章
相關標籤/搜索