1.引言
實體對應的英語單詞為Entity。提到實體,你可能立馬就想到了代碼中定義的實體類。在使用一些ORM框架時,比如Entity Framework,實體作為直接反映數據庫表結構的對象,就更尤為重要。特別是當我們使用EF Code First時,我們首先要做的就是實體類的設計。在DDD中,實體作為領域建模的工具之一,也是十分重要的概念。
但DDD中的實體和我們以往開發中定義的實體是同一個概念嗎?
不完全是。在以往未實施DDD的項目中,我們習慣于將關注點放在數據上,而非領域上。這也就說明了為什么我們在軟件開發過程中會首先做數據庫的設計,進而根據數據庫表結構設計相應的實體對象,這樣的實體對象是數據模型轉換的結果。
在DDD中,實體作為一個領域概念,在設計實體時,我們將從領域出發。
2.DDD中的實體
DDD中要求實體是唯一的且可持續變化的。意思是說在實體的生命周期內,無論其如何變化,其仍舊是同一個實體。唯一性由唯一的身份標識來決定的。可變性也正反映了實體本身的狀態和行為。
3. 唯一標識
舉個例子,在有雙胞胎的家庭里,家人都可以快速分辨開來。這得益于家人對雙胞胎性格和外貌的區分。然而鄰居卻不能,只能通過名字來區分。上小學后,學校里盡然有重名的,這時候就要取外號區分了。上大學后,要坐火車去學校,買票時就要用身份證號來區分了。
針對這個例子,如果我們要抽象出一個User實體,要如何定義其唯一標識呢?
其中性格、外貌、昵稱、身份證號都可以作為User實體的屬性,在某些場景下某個屬性就可以對對象進行區分。但為了確保標識的穩定性,我們只能將身份證號設為唯一身份標識。
3.1.唯一標識的類型
唯一標識的類型在不同的場景又有不同的要求。
主要可以分為有意義和無意義兩種。
在一個簡單的應用程序里,一個int類型的自增Id就可以作為唯一標識。優點就是占用空間小,查詢速度快。
而在一些業務當中,要求唯一標識有意義,通過唯一標識就能識別出一些基本信息,比如支付寶的交易號,其中就包含了日期和用戶ID。這種就屬于字符串類型的標識,這就對唯一標識的生成提出了挑戰。
在一些復雜的業務流程中,對唯一標識沒有要求,我們可以使用GUID類型來生成唯一標識,很顯然GUID占用空間就畢竟大,且不利于查詢。
3.2.唯一標識的生成時機
有某些場景下,唯一標識的生成時機也各不相同,主要分為即時生成和延遲生成。
即時生成,即在持久化實體之前,先申請唯一標識,再更新到數據庫。
延遲生成,即在持久化實體之后。
3.3.委派標識和領域標識
基于領域實體概念分析確定的唯一身份標識,我們可以稱為領域實體標識。
而在有些ORM工具,比如Hibernate、EF,它們有自己的方式來處理對象的身份標識。它們傾向于使用數據庫提供的機制,比如使用一個數值序列來生成識。在ORM中,委派標識表現為int或long類型的實體屬性,來作為數據庫的主鍵。很顯然,委派標識是為了迎合ORM而創建的,且委派標識和領域實體標識無任何關系。
那既然ORM需要委派標識,我們就可以創建一個實體基類來統一指定委派標識。而這個實體基類又被稱為層超類型。
3.3.1.實現層超類型
首先定義層超類型接口:
public interface IEntity
{
}
public interface IEntity<TPrimaryKey> : IEntity
{
TPrimaryKey Id { get; set; }
}
通過定義泛型接口,以支持自定義主鍵類型。
實現層超類型:
public class Entity : Entity<int>, IEntity
{
}
public class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
{
public virtual TPrimaryKey Id { get; set; }
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity<TPrimaryKey>))
{
return false;
}
//Same instances must be considered as equal
if (ReferenceEquals(this, obj))
{
return true;
}
var other = (Entity<TPrimaryKey>) obj;
//Must have a IS-A relation of types or must be same type
var typeOfThis = GetType();
var typeOfOther = other.GetType();
if (!typeOfThis.GetTypeInfo().IsAssignableFrom(typeOfOther) && !typeOfOther.GetTypeInfo().IsAssignableFrom(typeOfThis))
{
return false;
}
return Id.Equals(other.Id);
}
public override int GetHashCode()
{
return Id.GetHashCode();
}
public static bool operator ==(Entity<TPrimaryKey> left, Entity<TPrimaryKey> right)
{
if (Equals(left, null))
{
return Equals(right, null);
}
return left.Equals(right);
}
public static bool operator !=(Entity<TPrimaryKey> left, Entity<TPrimaryKey> right)
{
return !(left == right);
}
}
可以看到默認的委托標識為int類型。我們重寫了Equals,GetHashCode方法,以及==和!=兩個操作符。
通過這樣一種方式,我們進行約定,所有的實體必須繼承自Entity
,即可實現委托標識的統一定義。
4.可變性
解決了實體的唯一身份標識問題后,我們就可以保證其生命周期中的連續性,不管其如何變化。
那可變性說的是什么呢?可變性是實體的狀態和行為。
而實體的狀態和行為就要對具體的業務模型加以分析,提煉出通用語言,再基于通用語言來抽象成實體對應的屬性或方法。
我們拿訂單環節來舉例說明:
當顧客從購物車點擊結算時創建訂單,初始狀態為未支付狀態,支付成功后切換到正常狀態,此時可對訂單做發貨處理并置為已發貨狀態。當顧客簽收后,將訂單關閉。
從以上的通用語言的描述中(在通用語言的術語中,名詞用于給概念命名,形容詞用于描述這些概念,而動詞則表示可以完成的操作。)
我們可以提取訂單的相關狀態和行為:
- 訂單狀態:未支付、正常、已發貨、關閉。針對狀態,我們需定義一個狀態屬性即可。
- 訂單的行為:支付、發貨和關閉。針對行為,我們可以在實體中定義方法或創建單獨的領域服務來處理。
實體既然存在狀態和行為,就必然會與事件有所牽連。比如訂單支付成功后,需要知會商家發貨。這時我們就要追蹤訂單狀態的變化,而追蹤變化最實用的方法就是領域事件。關于領域事件,我們后續再講。
5.實體的驗證
驗證的目的是為了檢查模型的正確性和有效性。檢查的對象可以為某個屬性,也可以是整個對象,或是多個對象的組合。針對驗證的方式,不一而足,根據需要可自行發揮。
6.總結
實體作為領域建模的工具之一,唯一的身份標識是實體最基本的特征,其次是可變性。唯一身份標識和可變性也是用來區分實體和值對象的主要特征。
為了正確建立實體模型,我們需要將關注點從數據轉向領域,從業務模型中提煉通用語言,再基于通用語言分析其狀態和行為。
所以,我們可以認為:實體 = 唯一身份標識 + 可變性【狀態(屬性) + 行為(方法或領域事件或領域服務)】