一、什麼是實體 由標識來區分的對象稱為實體。 實體的定義隱藏了幾個信息: 兩個實體對象,只要它們的標識屬性值相等,哪怕標識屬性以外的所有屬性值都不相等,這兩個對象也認為是同一個實體,這意味著兩個對象是同一實體在其生命周期內的不同階段。 為了能正確區分實體,標識必須唯一。 實體的標識屬性值是不可變的, ...
一、什麼是實體
由標識來區分的對象稱為實體。
實體的定義隱藏了幾個信息:
- 兩個實體對象,只要它們的標識屬性值相等,哪怕標識屬性以外的所有屬性值都不相等,這兩個對象也認為是同一個實體,這意味著兩個對象是同一實體在其生命周期內的不同階段。
- 為了能正確區分實體,標識必須唯一。
- 實體的標識屬性值是不可變的,標識屬性以外的屬性值是可變的。如果標識值不大穩定,偶爾會變化,那麼就無法將該實體在生命周期內的所有變化關聯在一起,這可能導致嚴重的問題。
二、實體標識
從實體的定義可以發現,標識是實體的關鍵特征。關於標識,有幾個值得思考的問題。
1、將什麼選作標識
比如中國人都有身份證,身份證號碼是唯一的,那麼可能會有人使用身份證號作為實體標識。這看起來好像沒什麼問題,但身份證每隔N年就會換代,身份證號可能發生變化。這違反了標識不可變性和穩定性要求,所以不適合作為實體標識。
對於手工錄入流水號作為實體標識的情況,要用戶自己保證唯一性已經很困難,如果提供了修改標識的功能,將導致標識不穩定,如果不提供,用戶錄入錯誤就只能刪除後重新輸入,這就太不人道了。
通過程式自動生成一個有意義的流水號作為實體標識,並且不提供修改,這可能是可行的,對於唯一性要求,程式和資料庫可以保證,另外不允許修改,就可以保證穩定性。對於像訂單號一類的場景可能有效。
可以看到,使用有意義的值作為標識有一定風險,並且難度比較大,為了簡單和方便,生成一個無意義的唯一值作為標識更可行。
2、為標識選擇什麼類型
對於使用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腳本,定時在半夜整理一下碎片,也算一個勉強的辦法。(針對此種情況,可以參考abp框架中生成guid的類庫方法,其機制是可以預先生成guid,同時生成的guid是遞增的。)
如果生成一個有意義的流水號來作為標識,這時候標識類型就是一個字元串。
有些時候可能還要使用更複雜的組合標識,這一般需要創建一個值對象作為標識類型。
我目前一般都使用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
轉載鏈接:http://www.cnblogs.com/xiadao521/p/4104190.html