DDD理論學習系列(6)-- 實體

[DDD理論學習系列——案例及目錄:http://www.jianshu.com/p/6e2917551e63]


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.總結

實體作爲領域建模的工具之一,唯一的身份標識是實體最基本的特徵,其次是可變性。唯一身份標識和可變性也是用來區分實體和值對象的主要特徵。

爲了正確建立實體模型,我們需要將關注點從數據轉向領域,從業務模型中提煉通用語言,再基於通用語言分析其狀態和行爲。

所以,我們可以認爲:實體 = 唯一身份標識 + 可變性【狀態(屬性) + 行爲(方法或領域事件或領域服務)】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章