DDD分層架構之領域實體(基礎篇)


DDD分層架構之領域實體(基礎篇)

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

什么是實體

  由標識來區分的對象稱為實體。

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

  • 兩個實體對象,只要它們的標識屬性值相等,哪怕標識屬性以外的所有屬性值都不相等,這兩個對象也認為是同一個實體,這意味着兩個對象是同一實體在其生命周期內的不同階段。
  • 為了能正確區分實體,標識必須唯一。
  • 實體的標識屬性值是不可變的,標識屬性以外的屬性值是可變的。如果標識值不大穩定,偶爾會變化,那么就無法將該實體在生命周期內的所有變化關聯在一起,這可能導致嚴重的問題。

實體標識

  從實體的定義可以發現,標識是實體的關鍵特征。關於標識,有幾個值得思考的問題。

將什么選作標識

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

  對於手工錄入流水號作為實體標識的情況,要用戶自己保證唯一性已經很困難,如果提供了修改標識的功能,將導致標識不穩定,如果不提供,用戶錄入錯誤就只能刪除后重新輸入,這就太不人道了。

  通過程序自動生成一個有意義的流水號作為實體標識,並且不提供修改,這可能是可行的,對於唯一性要求,程序和數據庫可以保證,另外不允許修改,就可以保證穩定性。對於像訂單號一類的場景可能有效。

  可以看到,使用有意義的值作為標識有一定風險,並且難度比較大,為了簡單和方便,生成一個無意義的唯一值作為標識更可行。

為標識選擇什么類型

  對於使用Sql Server的同學,一般會傾向於使用int類型,映射到數據庫中的自增長int。它的優勢是簡單,唯一性由數據庫保障,占用空間小,查詢速度快。我之前也采用了很長時間,大部分時候很好用,不過偶爾會很頭痛。

  由於實體標識需要等到插入數據庫之后才創建出來,所以你在保存之前不可能知道標識值是多少,如果在保存之前需要拿到Id,唯一的方法是先插入數據庫,得到Id以后,再執行另外的操作,換句話說,需要把本來是同一個事務中的操作分成多個事務執行。

  使用自增長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 ) {
        }
    }
}
復制代碼

  完整單元測試代碼如下。

  單元測試代碼

  完整EntityBase代碼如下。

  EntityBase

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

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

  謝謝大家的持續關注,我的博客地址:http://www.cnblogs.com/xiadao521/

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

 

版權所有,轉載請注明出處  何鎮汐的技術博客
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM