領域驅動設計實戰--戰略建模

引子

  自從去年接觸DDD以來,閱讀了大量的相關書籍,看了園子裏面很多DDD方面的文章,也在實際項目中也有意無意的使用DDD的一些思想和方法。但總覺得這些知識太分散,沒有給自己思想帶來質的昇華。觀摩了園子裏面很多DDD的例子:

  等等還有很多,這些例子的質量都很高,圖文並茂,一堆讓人流口水的代碼示例。但每次看完例子後,我的迷茫就會又加深一分。因爲我覺得很多博文都只是打着DDD的幌子,還是按自己的原來理解方式寫代碼而已。並沒有給大家深入剖析DDD。別忘了,DDD是領域驅動設計,而不是領域驅動開發。在完成這些例子過程中,太重視出成果,出一個實際的代碼項目,而忽視了設計,即使有些設計,也只是停留在戰術建模的階段,並沒有站在戰略建模的高度來對需求進行分析,通常給出一個UML類關係圖,一個分層架構圖就開始突突突地來一堆代碼,最後還不忘貼兩個漂亮的UI截圖,引起無數粉絲追捧(額~~他們的粉絲中也有我,看來可以把我拉黑了)。用沃恩.弗農大神的話,這樣做只能算DDD-Lite,離真正的DDD還有段距離。

  於是我有了寫這篇文章的想法。

  所謂的戰略建模有兩點:界限上下文(Bounded Context)、上下文映射圖(Context Mapping)。

  來看一下它們的定義:

限界上下文:它是一個限定邊界的環境,在該環境中,每一個模型的概念(包括它的屬性和操作)都具有特殊的含義。它是戰略建模的核心。

上下文映射圖:通用使用框圖或代碼的方式來展現限界上下文之間的集成關係。

  它們爲什麼重要,我以dax.net大大的一個例子來說明,來看一下他的一篇文章EntityFramework之領域驅動設計實踐(三) ,文中內容大概是這樣安排的:

  1. 首先介紹他的例子:一個簡易的銷售系統;
  2. 給出UML實體框圖;
  3. 然後突突的給出一堆代碼;

  這是一個典型的戰術建模的例子。而且該文中有一句極其誤導DDD新人的話,我不得不吐槽一下:

上面的模型表述了領域模型中各個實體及其之間的關係。我們先不去討論整個系統的業務會是什麼樣的,我們先看看EF是如何支持實體和值對象概念的。

  它直接讓我們這些技術狂熱愛好者們瞬間偏離了DDD以業務爲核心,不依賴具體技術實現的初衷。DDD本來就是引導我們來解決業務上的問題,而不是來讓我們炫耀新技能的。如果只是介紹EF,那這篇文章是篇合格的文章。但,如果把它作爲DDD的文章,那它就是個反面教程。

  很不幸,本文開始的幾個鏈接也都被我劃到反面教程之列,而且類似的文章園子裏面還有很多。這些文章用來教大家寫代碼可以,但不能作爲DDD的教程。

 我的DDD設計之道

  就像前面說的,我們進行DDD的第一步,不應該是急着去創建實體模型,而應該站在更高的層面去理解需求,劃分領域。這裏我就以dax.net這個簡易的銷售系統爲例。首先我們來看看那篇文章中的模型圖(注意:這裏不是用這個模型圖,而僅僅是參考一下用來分析業務,就當這個模型圖是會說話的客戶吧!):

   

必須坐下來和客戶(就是上面這個模型圖)好好談談了,以下爲談話內容:

  把對話內容總結一下,我們可以看出需求業務大概是這樣的:

  • 客戶可以在本系統進行支付,並且可以使用多信用卡支付;
  • 爲客戶生成訂單;
  • 在現有的訂單基礎上可以進行退訂;
  • 管理客戶信息,主要是信用卡信息管理;
  • 管理產品信息;
  • 管理產品分類信息;

     來把這幾條業務進行劃分,得到原始領域結構圖:

  上圖只是對領域進行了劃分及確定限界上下文,各限界上下文的關係下面會有介紹。這裏共4個限界上下文:客戶上下文,訂單上下文,支付上下文及產品上下文。這裏有以下幾點要特別說明:

  • 通過對領域的劃分,優化了原有系統設計中Item實體即表示訂單項目,又表示產品的混淆的定義。在訂單上下文中,沒有產品這個概念,只有訂單項,訂單項的屬性數據(主要是產品名稱,單價)會在生成訂單項實體時從產品上下文中的產品獲取;
  • 支付上下文中沒有"客戶"這個概念,只有"帳戶"這個概念(圖中畫成帳號,在此糾正一下),同訂單中的訂單項,"帳戶"的屬性也會在生成帳戶實體時從其他上下文中獲取,這裏是從客戶上下文中獲取,"帳戶"是客戶的一個子集;
  • 隨着業務的增長,可以把退訂業務從訂單管理子域中劃分出來,作爲一個單獨子域,這裏暫時先不考慮;

  可以看出,在確定子域及限界上下文後,一些容易混淆的概念會逐漸得到清晰的描述,這樣可以方便開發團隊、業務人員及客戶之間的交流,而且還爲我們開發時劃分項目功能提供最直接的依據。

  我們進一步對該圖進行優化。來看一下限界上下文之間的關係,即上下文映射圖:

 

  圖中的實線連接,表示兩端的限界上下文之間存在聯繫。線上標註的U/D表示上游/下游。通常情況下:上游的限界上下文會爲下游提供訪問接口(或服務),下游使用一個防腐層獲取從上游接口傳過來的數據,然後轉化成本限界中使用的實體。

  舉個例子:產品上下文是訂單上下文的上游。當用戶進行產品選購時,會向訂單裏面添加訂單項(丫的,這裏沒設計購物車~~):

    class Order
    {
        private IList<Item> _items = new List<Item>(); 
        //支付上下文中的防腐層
        private IACLService _iaclService;

        public Order(IACLService aclService)
        {
            _iaclService = aclService;
        }

        /// <summary>
        /// 添加訂單項
        /// </summary>
        /// <param name="productid">產品ID</param>
        /// <param name="count">產品個數</param>
        public void AddItem(string productid, int count)
        {
            Item item = _iaclService.GetItemFor(productid);
            item.Count = count;
            _items.Add(item);
        }
    }

  在僞實現中,添加訂單項的函數AddItem參數用的是產品的id,而不是產品的具體信息,這樣就可以實現訂單項與產品的解耦。假設我們的產品管理子域是通過WCF用服務的方式實現,防腐層就可以通過訪問服務來獲取該產品,然後把它轉化成一個Item。從而實現訂單項的添加。

    DDD方面的代碼實現會在近期放出,敬請期待!

後記

  本文是站在個人理解角度去闡述DDD,在實現過程中也沒有涉及CQRS/AES等概念,只是想說明一下,如果使用DDD,請站在業務的角度,而不是技術的角度。

  關於學習DDD,強烈建議學習netfocusENode框架!

  本文如有冒犯之處還請海涵,歡迎拍磚,不過你要是進行人身攻擊,我也只能在心裏畫個圈圈詛咒你一下。

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