應用程序框架實戰十四:DDD分層架構之領域實體(基礎篇)

  上一篇,我介紹了本身在DDD分層架構方面的一些感想,本文開始介紹領域層的實體,代碼主要參考自《領域驅動設計C#2008實現》,另外參考了網上找到的一些示例代碼。數據庫

什麼是實體

  由標識來區分的對象稱爲實體。架構

  實體的定義隱藏了幾個信息:框架

  • 兩個實體對象,只要它們的標識屬性值相等,哪怕標識屬性之外的全部屬性值都不相等,這兩個對象也認爲是同一個實體,這意味着兩個對象是同一實體在其生命週期內的不一樣階段。
  • 爲了能正確區分實體,標識必須惟一。
  • 實體的標識屬性值是不可變的,標識屬性之外的屬性值是可變的。若是標識值不大穩定,偶爾會變化,那麼就沒法將該實體在生命週期內的全部變化關聯在一塊兒,這可能致使嚴重的問題。

實體標識

  從實體的定義能夠發現,標識是實體的關鍵特徵。關於標識,有幾個值得思考的問題。ide

什麼選做標識

  好比中國人都有身份證,身份證號碼是惟一的,那麼可能會有人使用身份證號做爲實體標識。這看起來好像沒什麼問題,但身份證每隔N年就會換代,身份證號可能發生變化。這違反了標識不可變性和穩定性要求,因此不適合做爲實體標識。函數

  對於手工錄入流水號做爲實體標識的狀況,要用戶本身保證惟一性已經很困難,若是提供了修改標識的功能,將致使標識不穩定,若是不提供,用戶錄入錯誤就只能刪除後從新輸入,這就太不人道了。性能

  經過程序自動生成一個有意義的流水號做爲實體標識,而且不提供修改,這多是可行的,對於惟一性要求,程序和數據庫能夠保證,另外不容許修改,就能夠保證穩定性。對於像訂單號一類的場景可能有效。單元測試

  能夠看到,使用有意義的值做爲標識有必定風險,而且難度比較大,爲了簡單和方便,生成一個無心義的惟一值做爲標識更可行。測試

爲標識選擇什麼類型

  對於使用Sql Server的同窗,通常會傾向於使用int類型,映射到數據庫中的自增加int。它的優點是簡單,惟一性由數據庫保障,佔用空間小,查詢速度快。我以前也採用了很長時間,大部分時候很好用,不過偶爾會很頭痛。ui

  因爲實體標識須要等到插入數據庫以後才建立出來,因此你在保存以前不可能知道標識值是多少,若是在保存以前須要拿到Id,惟一的方法是先插入數據庫,獲得Id之後,再執行另外的操做,換句話說,須要把原本是同一個事務中的操做分紅多個事務執行。this

  使用自增加int類型的第二個毛病是,若是須要合併同一個實體對應的多個數據表記錄,悲劇就會發生。好比你如今把一個實體對應的記錄水平分區到多個數據庫的表中,因爲Id是自增加的,每一個表都會從1開始自增,你要合併到一個表中,Id就會發生衝突。因此對於比較大點的項目,使用自增加int類型是有一些風險的。

  對於比較小,且不是太複雜的項目,使用自增加int類型是個不錯的選擇,但若是你常常碰到上面提到的問題,說明你須要從新選擇標識類型了。

  要解決以上問題,最簡單的方法是選擇Guid做爲標識類型。

  它的主要優點是生成Guid很是容易,不管是Js,C#仍是在數據庫中,都能輕易的生成出來。另外,Guid的惟一性很強,基本不可能生成出兩個相同的Guid。

  Guid類型的主要缺點是佔用空間太大。另外實體標識通常映射到數據庫的主鍵,而Sql Server會默認把主鍵設成彙集索引,因爲Guid的不連續性,這可能致使大量的頁拆分,形成大量碎片從而拖慢查詢。一個解決辦法是使用Sql Server來生成Guid,它能夠生成連續的Guid值,但這又回到了老路,只有插入數據庫你才知道具體的Id值,因此行不通。另外一個解決辦法是把彙集索引移到其它列上,好比建立時間。若是你打算把彙集索引繼續放到Guid標識列上,能夠觀察到碎片通常都在90%以上,寫一個Sql腳本,定時在半夜整理一下碎片,也算一個勉強的辦法。

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

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

  我目前通常都使用Guid做爲標識類型,偶爾使用字符串類型。

  對於須要更詳細的瞭解實體標識,請參考《企業應用架構模式》標識域一節。

實體層超類型的實現

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

  爲了下降依賴性,如今須要在本系列應用程序框架的VS解決方案中增長一個類庫Util.Domains和單元測試項目Util.Domains.Tests,並使用解決方案文件夾進行分類,以下圖所示。

 

  各程序集的依賴關係以下圖所示。 

  實體基類能夠取名爲EntityBase,它應該是一個抽象類,具備一個名爲Id的屬性。若是採用int做爲標識類型,代碼多是這樣。

namespace Util.Domains {
    /// <summary>
    /// 領域實體
    /// </summary>
    public abstract class EntityBase{
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( int id ) {
            Id = id;
        } 

        /// <summary>
        /// 標識
        /// </summary>
        public int Id { get; private set; }
    }
}

  觀察上面的代碼,這裏要考慮的關鍵問題是Id的set屬性是否應該公共出來。根據前面的介紹,實體標識應該是不可變的,若是把Id的set屬性設爲公開,那麼任何人均可以隨時很方便的修改它,從而破壞了封裝性。

  那麼,把Id的set屬性設成私有,外界確實沒法修改它,設置Id的惟一方法是在建立這個實體時,從構造函數傳進來。但這會致使哪些問題?先看看ORM,它須要將數據庫中的Id列映射到實體的Id屬性上,若是set被設爲私有,還能映射成功嗎。經過測試,通常的ORM都具有映射私有屬性的能力,好比EF,因此這不是問題。再來看看錶現層,好比Mvc,Mvc提供了一個模型綁定功能,能夠把表現層的數據映射到實體的屬性上,若是屬性是私有的會如何?測試之後,發現只有包含public 的set屬性才能夠映射成功,甚至字段都不行。再測試Wpf的雙向綁定,也基本如此。因此把Id的set屬性設爲私有,將致使實體在表現層沒法直接使用,須要經過Dto或ViewModel進行中轉。

  因此你須要在封裝性和易用性上做出權衡,若是你但願更高的健壯性,那就把Id的set屬性隱藏起來,不然直接把Id暴露出來,經過約定告訴你們不要在建立了實體以後修改Id的值。因爲本系列準備演示Dto的用法,因此會把Id setter隱藏起來,並經過Dto來轉換。若是你須要更方便,請刪除Id setter上的private。

  如今Id類型爲int,若是要使用Guid類型的實體,咱們須要建立另外一個實體基類。

namespace Util.Domains {
    /// <summary>
    /// 領域實體
    /// </summary>
    public abstract class EntityBase{
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( Guid id ) {
            Id = id;
        } 

        /// <summary>
        /// 標識
        /// </summary>
        public Guid Id { get; private set; }
    }
}

  它們的惟一變化是Id數據類型不一樣,咱們能夠把Id類型設爲object,從而支持全部類型。

namespace Util.Domains {
    /// <summary>
    /// 領域實體
    /// </summary>
    public abstract class EntityBase{
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( object id ) {
            Id = id;
        } 

        /// <summary>
        /// 標識
        /// </summary>
        public object Id { get; private set; }
    }
}

  但弱類型的object將致使裝箱和拆箱,另外也不太易用,這時候是泛型準備登場的時候了。

namespace Util.Domains {
    /// <summary>
    /// 領域實體
    /// </summary>
    /// <typeparam name="TKey">標識類型</typeparam>
    public abstract class EntityBase<TKey> {
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( TKey id ) {
            Id = id;
        } 

        /// <summary>
        /// 標識
        /// </summary>
        [Required]
        public TKey Id { get; private set; }
    }
}

  將標識類型經過泛型參數TKey傳進來,因爲標識類型能夠任意,因此不須要進行泛型約束。另外在Id上方加了一個Required特性,當Id爲字符串或其它引用類型的時候,就能派上用場了。

  下面要解決的問題是實體對象相等性比較,須要重寫Equals,GetHashCode方法,另外須要重寫==和!=兩個操做符重載。

        /// <summary>
        /// 相等運算
        /// </summary>
        public override bool Equals( object entity ) {
            if ( entity == null )
                return false;
            if ( !( entity is EntityBase<TKey> ) )
                return false;
            return this == (EntityBase<TKey>)entity;
        }

        /// <summary>
        /// 獲取哈希
        /// </summary>
        public override int GetHashCode() {
            return Id.GetHashCode();
        }

        /// <summary>
        /// 相等比較
        /// </summary>
        /// <param name="entity1">領域實體1</param>
        /// <param name="entity2">領域實體2</param>
        public static bool operator ==( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) {
            if ( (object)entity1 == null && (object)entity2 == null )
                return true;
            if ( (object)entity1 == null || (object)entity2 == null )
                return false;
            if ( entity1.Id == null )
                return false;
            if ( entity1.Id.Equals( default( TKey ) ) )
                return false;
            return entity1.Id.Equals( entity2.Id );
        }

         /// <summary>
        /// 不相等比較
        /// </summary>
        /// <param name="entity1">領域實體1</param>
        /// <param name="entity2">領域實體2</param>
        public static bool operator !=( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) {
            return !( entity1 == entity2 );
        }    

  在操做符==的代碼中,有一句須要注意,entity1.Id.Equals( default( TKey ) ),好比,一個實體的標識爲int類型,這個實體在剛建立的時候,Id默認爲0,另外建立一個該類的實例,Id也爲0,那麼這兩個實體是相等仍是不等?從邏輯上它們是不相等的,屬於不一樣的實體, 只是標識目前尚未建立,可能須要等到保存到數據庫中才能產生。這有什麼影響呢?當進行某些集合操做時,若是你發現操做N個實體,但只有一個實體操做成功,那頗有多是由於這些實體的標識是默認值,而你的相等比較沒有識別出來,這一句代碼可以解決這個問題。

  考慮領域實體基類還能幫咱們乾點什麼,其實還不少,好比狀態輸出、初始化、驗證、日誌等。下面先來介紹一下狀態輸出。

  當我在操做每一個實體的時候,我常常須要在日誌中記錄完整的實體狀態,即實體全部屬性名值對的列表。這樣方便我在查找問題的時候,能夠了解某個實體當時是個什麼狀況。

  要輸出實體的狀態,最方便的方法是重寫ToString,而後把實體狀態列表返回回來。這樣ToString方法將變得有意義,由於它輸出一個實體的類名基本沒什麼用。

  要輸出實體的所有屬性值,一個辦法是經過反射在基類中進行,但這可能會形成一點性能降低,因爲經過代碼生成器能夠輕鬆生成這個操做,因此我沒有采用反射的方法。

        /// <summary>
        /// 描述
        /// </summary>
        private StringBuilder _description;

        /// <summary>
        /// 輸出領域對象的狀態
        /// </summary>
        public override string ToString() {
            _description = new StringBuilder();
            AddDescriptions();
            return _description.ToString().TrimEnd().TrimEnd( ',' );
        } 

        /// <summary>
        /// 添加描述
        /// </summary>
        protected virtual void AddDescriptions() {
        }

        /// <summary>
        /// 添加描述
        /// </summary>
        protected void AddDescription( string description ) {
            if ( string.IsNullOrWhiteSpace( description ) )
                return;
            _description.Append( description );
        } 

        /// <summary>
        /// 添加描述
        /// </summary>
        protected void AddDescription<T>( string name, T value ) {
            if ( string.IsNullOrWhiteSpace( value.ToStr() ) )
                return;
            _description.AppendFormat( "{0}:{1},", name, value );
        }

  在子類中須要重寫AddDescriptions方法,並在該方法中調用AddDescription這個輔助方法來添加屬性名值對的描述。

  因爲驗證和日誌等內容須要一些公共操做類提供幫助,因此放到後面幾篇進行介紹。

  爲了使泛型的EntityBase<TKey>用起來更簡單一點,我建立了一個EntityBase,它從泛型EntityBase<Guid>派生,這是由於我如今主要使用Guid做爲標識類型。

namespace Util.Domains {
    /// <summary>
    /// 領域實體基類
    /// </summary>
    public abstract class EntityBase : EntityBase<Guid> {
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( Guid id )
            : base( id ) {
        }
    }
}

  完整單元測試代碼以下。

using System;

namespace Util.Domains.Tests.Samples {
    /// <summary>
    /// 測試實體
    /// </summary>
    public class Test : EntityBase {
        /// <summary>
        /// 初始化
        /// </summary>
        public Test()
            : this( Guid.NewGuid() ) {
        }

        /// <summary>
        /// 初始化員工
        /// </summary>
        /// <param name="id">員工編號</param>
        public Test( Guid id )
            : base( id ) {
        }

        /// <summary>
        /// 姓名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 添加描述
        /// </summary>
        protected override void AddDescriptions() {
            AddDescription( "Id:"+ Id + "," );
            AddDescription( "姓名", Name );
        }
    }
}

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Util.Domains.Tests.Samples;

namespace Util.Domains.Tests {
    /// <summary>
    /// 實體基類測試
    /// </summary>
    [TestClass]
    public class EntityBaseTest {
        /// <summary>
        /// 測試實體1
        /// </summary>
        private Test _test1;
        /// <summary>
        /// 測試實體2
        /// </summary>
        private Test _test2;

        /// <summary>
        /// 測試初始化
        /// </summary>
        [TestInitialize]
        public void TestInit() {
            _test1 = new Test();
            _test2 = new Test();
        }

        /// <summary>
        /// 經過構造方法設置標識
        /// </summary>
        [TestMethod]
        public void TestId() {
            Guid id = Guid.NewGuid();
            _test1 = new Test( id );
            Assert.AreEqual( id, _test1.Id );
        }

        /// <summary>
        /// 新建立的實體不相等
        /// </summary>
        [TestMethod]
        public void TestNewEntityIsNotEquals() {
            Assert.IsFalse( _test1.Equals( _test2 ) );
            Assert.IsFalse( _test1.Equals( null ) );

            Assert.IsFalse( _test1 == _test2 );
            Assert.IsFalse( _test1 == null );
            Assert.IsFalse( null == _test2 );

            Assert.IsTrue( _test1 != _test2 );
            Assert.IsTrue( _test1 != null );
            Assert.IsTrue( null != _test2 );
        }

        /// <summary>
        /// 當兩個實體的標識相同,則實體相同
        /// </summary>
        [TestMethod]
        public void TestEntityEquals_IdEquals() {
            Guid id = Guid.NewGuid();
            _test1 = new Test( id );
            _test2 = new Test( id );
            Assert.IsTrue( _test1.Equals( _test2 ) );
            Assert.IsTrue( _test1 == _test2 );
            Assert.IsFalse( _test1 != _test2 );
        }

        /// <summary>
        /// 測試狀態輸出
        /// </summary>
        [TestMethod]
        public void TestToString() {
            _test1 = new Test { Name = "a" };
            Assert.AreEqual( string.Format( "Id:{0},姓名:a", _test1.Id ), _test1.ToString() );
        }
    }
}
單元測試代碼

  完整EntityBase代碼以下。

using System.ComponentModel.DataAnnotations;
using System.Text;

namespace Util.Domains {
    /// <summary>
    /// 領域實體
    /// </summary>
    /// <typeparam name="TKey">標識類型</typeparam>
    public abstract class EntityBase<TKey> {

        #region 構造方法

        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( TKey id ) {
            Id = id;
        }

        #endregion

        #region 字段

        /// <summary>
        /// 描述
        /// </summary>
        private StringBuilder _description;

        #endregion

        #region Id(標識)

        /// <summary>
        /// 標識
        /// </summary>
        [Required]
        public TKey Id { get; private set; }

        #endregion

        #region Equals(相等運算)

        /// <summary>
        /// 相等運算
        /// </summary>
        public override bool Equals( object entity ) {
            if ( entity == null )
                return false;
            if ( !( entity is EntityBase<TKey> ) )
                return false;
            return this == (EntityBase<TKey>)entity;
        }

        #endregion

        #region GetHashCode(獲取哈希)

        /// <summary>
        /// 獲取哈希
        /// </summary>
        public override int GetHashCode() {
            return Id.GetHashCode();
        }

        #endregion

        #region ==(相等比較)

        /// <summary>
        /// 相等比較
        /// </summary>
        /// <param name="entity1">領域實體1</param>
        /// <param name="entity2">領域實體2</param>
        public static bool operator ==( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) {
            if ( (object)entity1 == null && (object)entity2 == null )
                return true;
            if ( (object)entity1 == null || (object)entity2 == null )
                return false;
            if ( entity1.Id == null )
                return false;
            if ( entity1.Id.Equals( default( TKey ) ) )
                return false;
            return entity1.Id.Equals( entity2.Id );
        }

        #endregion

        #region !=(不相等比較)

        /// <summary>
        /// 不相等比較
        /// </summary>
        /// <param name="entity1">領域實體1</param>
        /// <param name="entity2">領域實體2</param>
        public static bool operator !=( EntityBase<TKey> entity1, EntityBase<TKey> entity2 ) {
            return !( entity1 == entity2 );
        }

        #endregion

        #region ToString(輸出領域對象的狀態)

        /// <summary>
        /// 輸出領域對象的狀態
        /// </summary>
        public override string ToString() {
            _description = new StringBuilder();
            AddDescriptions();
            return _description.ToString().TrimEnd().TrimEnd( ',' );
        }

        /// <summary>
        /// 添加描述
        /// </summary>
        protected virtual void AddDescriptions() {
        }

        /// <summary>
        /// 添加描述
        /// </summary>
        protected void AddDescription( string description ) {
            if ( string.IsNullOrWhiteSpace( description ) )
                return;
            _description.Append( description );
        }

        /// <summary>
        /// 添加描述
        /// </summary>
        protected void AddDescription<T>( string name, T value ) {
            if ( string.IsNullOrWhiteSpace( value.ToStr() ) )
                return;
            _description.AppendFormat( "{0}:{1},", name, value );
        }

        #endregion
    }
}

using System;

namespace Util.Domains {
    /// <summary>
    /// 領域實體基類
    /// </summary>
    public abstract class EntityBase : EntityBase<Guid> {
        /// <summary>
        /// 初始化領域實體
        /// </summary>
        /// <param name="id">標識</param>
        protected EntityBase( Guid id )
            : base( id ) {
        }
    }
}
EntityBase

  爲了完成實體基類的驗證,我須要先提供兩個公共操做類,即驗證和自定義異常類,待把這兩個類完成後,咱們再繼續介紹實體基類在驗證方面的支持。

  .Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。若是發現代碼中有BUG,請及時告知,我將迅速修復。

  謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/

  下載地址:http://files.cnblogs.com/xiadao521/Util.2014.11.17.1.rar

相關文章
相關標籤/搜索