如何運用DDD - 實體

如何運用DDD - 實體

概述

本文將介紹領域驅動設計(DDD)戰術模式中另外一個常見且很是重要的概念 - 實體。相對戰術模式中其餘的一些概念(例如 值對象、領域服務等)來講,實體應該比較容易讓人理解和運用。可是咱們如何去發現所在領域中的實體呢?如何保證創建的實體是富含行爲的?實體運用時又有那些注意的細節呢?本文將從不一樣的角度來帶你們從新認識一下「實體」這個概念,而且給出相應的代碼片斷(本教程的代碼片斷都使用的是C#,後期的實戰項目也是基於 DotNet Core 平臺)。數據庫

何爲實體

按照國際慣例呢,咱們先吹牛。直接來看看原著《領域驅動設計:軟件核心複雜性應對之道》 中對實體的解釋:編程

  • 實體(Entity,又稱爲Reference Object) 不少對象不是經過他們的屬性定義的,而是經過一連串的連續事件和標識定義的。
  • 主要由標識定義的對象被稱爲ENTITY。

上面的兩句話多讀了幾遍,好像這個定義仍是可以理解嘛。不像上一篇文章 如何運用DDD - 值對象 中的概念那麼深奧。說白了,上面就是說明了一個問題,只要你所發現的事物/對象有一個惟一的標識,那麼它可能就是實體了。而惟一的標識就是咱們代碼中快寫爛了的那個ID。小程序

似曾相識

來想一下,咱們在以傳統的設計思路和開發過程當中,咱們會在什麼狀況下爲一個對象賦予一個ID呢?給它賦予這個ID的做用呢?通常來講咱們的目的無非就是 一、爲了區分本對象,若是是在數據庫中,那就是爲了區分本條數據和另一條數據,而這個ID也每每做爲主鍵而存在 二、加個索引吧,來提高關聯查找速度。因此咱們若是將數據庫中的表映射到咱們的代碼中以類的形式呈現的時候,它可能就是這個樣子:c#

//旅行的行程
public class Itinerary
{
    public int ID { get; set; }

    //參加本次旅行的人員
    public List<Person> Participants { get; set; }

    //旅行的地點
    public List<string> Places { get; set; } 

    //關於該行程的備註筆記信息
    public string  Note { get; set; } 

    //旅行開始時間
    public DateTime StartTime { get; set; }

    //旅行開始時間
    public DateTime? EndTime { get; set; }

    //旅行的狀態(進行中 or 已完成)
    public int Status { get; set; }
}

上面的代碼對咱們來講應該絲毫都不陌生,咱們創建了一個旅行行程的類,至於爲何咱們會選取旅行行程,而不是各個博客都出現的以訂單啊電商平臺做爲案例。那是由於在後期咱們會一塊兒動手來實現一個旅行記帳的微信小程序,而且藉助於咱們慢慢所學習到的DDD理論做爲基礎,開發屬於咱們本身的領域驅動框架,固然項目也是基於 DotNet Core(版本應該是3.x)。微信小程序

好了,仍是回到咱們這個例子,來思考一下ID出現的目的。你可能會說:「這還不簡單嗎?老夫縱橫代碼界多年,你如今還來問我這個問題!ID確定是用來區分的呀,行程千千萬萬,我要找出這一條行程確定須要這個ID了呀。」 是的,這是一個毫無爭議的問題。咱們須要一個惟一的身份標識來區別對象之間的差別。DDD中實體的這一點與咱們平時所接觸的類的ID有殊途同歸之妙,因此本文開頭也說了實體多是相對其餘戰術概念最爲讓人理解的。微信

你肯定它真的須要ID嗎

還記得咱們在上一篇文章 如何運用DDD - 值對象 中所提到過的一個問題嗎? 「當前上下文的值對象多是另外一個上下文的實體」。因此說,當前你所斷定的實體必定是基於領域當前環境(上下文)的。脫離了該環境以後,一切都將存在變數。一樣的事物(對象),在當前環境須要一個惟一標識來識別它,而在另外一個環境中可能這個惟一標識對它來講是沒有意義的,則實體就有可能成爲了值對象。請考慮下面的這個例子:架構

在一個銀行業應用程序中,一位顧客可能會在她的銀行帳戶中放入100美圓。當她將來某一天提取她這100美圓時,相較於她存進銀行的錢,她可能會收到不一樣的鈔票或硬幣。不過,這一差別是可有可無的,由於資金的身份不重要;顧客只關心資金的價值。因此在這個領域中,資金無疑是一個值對象。但在另外一個領域中,好比涉及鈔票印刷製做或鈔票可追溯性的行業,個體鈔票或硬幣的身份實際上可能就是一個重要的領域概念了。因此每一張鈔票都會是一個具備惟一標識符的實體框架

運用實體

結合值對象

千萬不要忘記了咱們上一章所學習到了的值對象:在實體的內部,除了它本身的惟一標識ID以外,也許還有許許多多代表它屬性的東西,而這些東西每每能夠經過使用值對象來標識。
接下來讓咱們來改寫一下上面的Itinerary類:學習

public class Itinerary
{
    public int ID { get; set; }

    public List<Person> Participants { get; set; }

    public List<Address> Places { get; set; } 

    public ItineraryNote  Note { get; set; } 

    public ItineraryTime TripTime { get; set; }

    public ItineraryStatus Status { get; set; }
}

public class ItineraryNote
{
    public string Content { get; set; }
    public DateTime NoteTime { get; set; }

    public ItineraryNote(string content)
    {
        Content = content;
        NoteTime = DateTime.Now;
    }
}

爲實體賦予它的行爲

當對象創建好了以後,爲了實現咱們的業務邏輯處理,咱們須要對實例化的對象進行操做。如今咱們爲該系統提出第一個需求:用戶能夠修改行程中的備註信息。
回到咱們的初版代碼中,若是咱們須要處理這個操做,咱們會怎麼作呢?

itineraryInstance.Note = "this is my new note info";

是否是會像上面這樣,將須要添加的值賦予實例化的對象呢。 這種操做,對咱們如今正在進行的編程習慣來講,是再正常不過了。

那麼咱們來思考,若是咱們的項目有多處須要對「備註信息」處理呢。則對該屬性的變動將被散落在代碼各處。而當咱們對該需求進行了一個加強驗證時,好比此時咱們須要增長:用戶修改行程中的備註信息時,只容許用戶錄入200個字之內的文本。 OMG,此時咱們須要去查找全部散落的片斷,而且爲他加上驗證。

從另外個角度來看,第一個版本咱們所創建的類,咱們沒法經過僅僅查看它自己就能讀懂有關旅行行程有關的業務,咱們僅僅知道它具備起始時間,備註信息等,而對他們應該如何相互做用無從所知。
因此這種僅僅具備類的屬性,或者說以POCO呈現的類型,咱們稱之爲「貧血模型」

接下來,咱們回到第二版代碼中,咱們爲它賦予屬於它的行爲。從需求中咱們得知了,行程的備註信息是能夠修改的,而備註信息是屬於行程的,所以修改備註信息改行爲理應屬於行程自己。咱們稍微改動代碼:

public class Itinerary
{
    public int ID { get; set; }

    public List<Person> Participants { get; set; }

    public List<Address> Places { get; set; } 

    public ItineraryNote  Note { get; set; } 

    public ItineraryTime TripTime { get; set; }

    public ItineraryStatus Status { get; set; }

    //ctor

    public void ChangeNote(string content)
    {
        if(content.Length > 200 )
            throw new NoteIsOverlengthException();
        Note  = new ItineraryNote(content);
    }
}

此時咱們爲Itinerary賦予了一個ChangeNote的行爲,當外界須要更改備註時,則只需經過調用改方法既能夠實現,並且當展開其餘開發人員閱讀此類時,也會清楚的明白,業務上容許用戶更改200字之內的備註。

可是,咱們依然有一個地方美中不足,我想你可能也發現了:屬性仍是對外暴露的! 對,也就是說,咱們除了經過類公開的行爲修改類自身的屬性外,咱們還能夠在外界隨意更改。這顯然不符合咱們設計的初衷。所以咱們能夠將全部屬性的set私有化。因此,必定要注意,咱們在考慮實體的時候,必定要知道「實體是高度內聚和自治的」(敲重點!!!!!)。

固然,有的開發者還會嘗試另外的寫法,讓實體徹底自治,將上面的代碼中的屬性,所有轉變爲私有的字段,外界只能經過公開的行爲來對實體進行處理。

public class Itinerary
{
    public int ID { get; set; }

    private List<Person> participants;

    private List<Address> places;

    private ItineraryNote  note;

    private ItineraryTime tripTime;

    private ItineraryStatus status;

    //ctor

    public void ChangeNote(string content)
    {
        if(content.Length > 200 )
            throw new NoteIsOverlengthException();
        note  = new ItineraryNote(content);
    }
}

可是當外界須要獲取該實體的值,或者須要ORM映射的時候可能就不是很友好了,不過你可使用相似於像 備忘錄模式 的快照方法來處理。後期咱們也會採用這種模式來實現部分案例。

經過將實體賦予它應用的行爲所創建出來的實體咱們稱爲「充血模型」。那麼貧血模型好仍是充血模型好呢? 不少同窗確定會說,這還用問嗎,確定是充血模型啦。 其實這個答案並無一個真正的答案,實體自身的行爲是經過咱們對領域的慢慢分析(多是經過與領域專家溝通)得來的,若是由於爲了使用充血模型而盲目的將一些不屬於實體的行爲賦予給它,只會讓實體變的更加混亂,從而得不償失。因此,此時的貧血模型並不意味着一直是貧血模型,後期隨着領域的深刻它可能會不斷豐富屬於自身的行爲。

嘗試轉移一部分行爲給值對象

保持實體專一於身份這一職責很重要,由於這樣會避免它們變得臃腫————這是它們將許多相關行爲拉到一塊兒時容易掉入的陷阱。實現這一專一須要將相關行爲委託給值對象和領域服務(領域服務也將在後期的文章中進行介紹)。
來考慮一下最近一版的代碼,咱們已經將行爲劃分給了Itinerary了,可是仔細看一看,咱們在後期增長需求時增長了一條驗證的規則,那麼這個規則咱們能夠轉移給值對象嗎? 答案是,能夠的。並且轉移是有必要的,由於對備註的效驗這一行爲每每應該屬於它自身。就比如機器啓動時的自我效驗,這一行爲是屬於操做者仍是機器本身呢?
因此咱們來將部分行爲轉移給值對象,優化後的代碼多是這樣的:

public class Itinerary
{
    public int ID { get; set; }

    public List<Person> Participants { get; set; }

    public List<Address> Places { get; set; } 

    public ItineraryNote  Note { get; set; } 

    public ItineraryTime TripTime { get; set; }

    public ItineraryStatus Status { get; set; }

    //ctor

    public void ChangeNote(string content)
    {
        Note  = new ItineraryNote(content);
    }
}

public class ItineraryNote
{
    public string Content { get; set; }
    public DateTime NoteTime { get; set; }

    public ItineraryNote(string content)
    {
        if(content.Length > 200 )
            throw new NoteIsOverlengthException();
        Content = content;
        NoteTime = DateTime.Now;
    }
}

願景是美好的 現實是殘酷的

到這裏,咱們彷彿真的一路順風:創建了屬於本身的實體,而且融合了該有的值對象,實體的行爲也被高度內聚在了其中。那是否是咱們直接就能夠將DDD落地了呢? 很差意思,就如同這個小標題同樣,現實真的是很是殘酷的。若是單單從代碼閱讀和業務處理上來講,咱們可能確實已經成功了,可是!!!咱們須要保存咱們的數據,也就是持久化。由於實體中包含了大量的值對象,全部值對象持久化所面臨的問題,它都會遇到,甚至是讓難度翻倍!有關值對象持久化的難點能夠參考上一篇文章 如何運用DDD - 值對象

回看咱們最後一版代碼,咱們有兩個集合的屬性(Participants、Places)。單一的值對象的持久化已經讓咱們頭痛了,如今咱們不得不面對持久化值對象集合的問題。假如你經過使用EF Core這類的ORM框架來進行持久化操做,你會發現咱們不得不爲List中的值對象加上一個ID,此時擁有了惟一標示的值對象顯然已經成爲了實體,這是很是可怕的一件事。咱們辛辛苦苦創建的領域模型在最後一步落地時竟然成爲改變了,這每每也是DDD落地困難的一個重要緣由,被ORM框架或者關係型數據庫所限制,致使領域模型不斷被打亂,重構領域模型變得愈來愈四不像,最終又寫回了傳統的三層架構或者面向數據庫建模。

可是至少在如今,請相信本身的所見,認真考慮和發現你項目領域所擁有的值對象和實體,不要由於知道持久化的問題而放棄和妥協,這也是咱們開發者應有的勇氣。在後面的文章中,咱們會關於值對象和實體的一些問題提出解決辦法,固然包括持久化的問題。

總結

本文咱們介紹了實體的概念以及怎麼去運用實體到實際代碼中,請牢記前人爲咱們提供的有關實體的經驗:好比「實體必定是基於領域當前環境(上下文)的」「實體是高度內聚和自治的」「應該專一於實體的行爲而非數據」等等。後面的文章會爲你們帶來實體和值對象的一些注意事項以及領域服務的內容。

相關文章
相關標籤/搜索