上一篇,我介紹了本身在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 ) { } } }
爲了完成實體基類的驗證,我須要先提供兩個公共操做類,即驗證和自定義異常類,待把這兩個類完成後,咱們再繼續介紹實體基類在驗證方面的支持。
.Net應用程序框架交流QQ羣: 386092459,歡迎有興趣的朋友加入討論。若是發現代碼中有BUG,請及時告知,我將迅速修復。
謝謝你們的持續關注,個人博客地址:http://www.cnblogs.com/xiadao521/
下載地址:http://files.cnblogs.com/xiadao521/Util.2014.11.17.1.rar