1. 程式人生 > 程式設計 >領域驅動設計DDD之概覽

領域驅動設計DDD之概覽

在從事開發多年之後,你是否會感覺自己只是一個業務CRUD Boy,並認為業務沒有多少技術含量。你是否會陷入業務的泥潭中,各種複雜交錯的業務規則使得程式碼開始腐爛開始失控,專案開始變得難以維護,迭代舉步維艱。如果你開始意識到這個問題的話,那麼我十分推薦你開始學習領域驅動設計面向領域建模的設計方式。

DDD是什麼呢?

DDD即Domain Driven Design,翻譯成中文的話就是領域驅動設計,首先我們應該先理解這裡的領域是什麼意思?假設公司內部正在開發一套電商平臺,而電商平臺中包含了庫存、訂單、商品等核心業務。這些核心業務邏輯其實呈現的就是電商平臺領域。通俗的理解就是一整套體系的業務知識即代表了一個領域。好比線上教育平臺,它需要有一套體系的業務,包括招生、線上教學、課程等內容。我們將這些業務抽象出領域模型,而這些領域模型表達了產品經理所闡述的業務需求,我們反覆地用這些領域模型與產品經理進行討論溝通最終確定初步的領域模型。再使用初步的領域模型指導程式碼設計開發。

簡而言之就是:

業務知識 --> 領域模型 --> 專案設計與程式碼開發

不同的電商平臺的核心業務邏輯大都是相似的,這部分領域知識是可以進行復用,區別在於不同公司使用的不同的程式語言,不同的前端控制框架,資料庫框架。採用領域驅動設計的好處在於專案以領域模型為核心,而Spring MVC、Struts等前端控制框架或者Hibernate、Mybatis物件資料庫框架屬於外圍技術基礎,領域模型其實並不與這些基礎技術產生耦合,所以在領域模型不變的情況下,我們是很容易對我們的基礎設施進行更換的。

那我們之前的開發也是有這些業務邏輯支撐?那我們之前傳統的開發模式為什麼不能稱之為面向領域驅動設計呢?回想一下我們之前的程式碼開發,比如一個實現一個購物車下訂單的功能,我們根據訂單表的欄位,依次用商品欄位、金額欄位、使用者欄位等拼湊出訂單表中的一條記錄,然後寫入到訂單表當中去。其實我們是面向資料庫在進行開發,以資料庫為重心,而業務邏輯零散地分佈在各個Service當中去,採用的是面向過程的程式設計方式而不是面向物件的方式,也就沒有形成一套有機的業務邏輯。

而面向領域驅動設計則不一樣,它以領域為重心,以剛才的購物車下訂單功能為例子。在DDD當中,會將購物車相關的業務邏輯封裝到一個ShoppingCart物件中,並直接呼叫shoppingCart.takeOrder()下訂單的方法,程式碼的重心從生成訂單表中的記錄轉移到購物車物件本身,而具體資料庫中如何生成這條記錄並不屬於我們的核心業務邏輯,它被下放到基礎設施層,由Repository或者Dao等資料互動物件負責去持久化我們對領域模型下達的指令所產生的資料庫變化。

專案中的程式碼通過不同領域模型之間的配合體現了領域專家的業務意圖,使得系統內部形成一套自執行的有機整體。在以往面向過程式的開發方式,我們很難讓領域專家或者產品經理直接看懂我們的業務邏輯,而DDD的優勢在於shoppingCart.takeOrder()這種類似白話的方式直接體現出業務含義。而我們程式碼中的領域模型和領域專家口中的領域模型是一致的。甚至我們可以在沒有基礎設施技術支援的情況下,直接建模領域模型開始編寫程式碼並測試驗證業務邏輯。

領域模型總結

  • 抽象了領域內的核心概念,並建立概念之間的關係;
  • 領域模型維護了領域內的資料之間一致性的,也即我們的業務規則;

為什麼我們需要DDD

  1. 通過統一語言使得我們開發人員與領域專家(產品經理)能夠更好的溝通。更準確的表達我們的業務,而這些業務程式碼按照正如領域專家所想的那樣工作。
  2. 通過戰術設計將業務知識進行集中,濃縮在領域模型當中。
  3. 以往的業務程式碼中,我們總需要嵌入許多註釋,因為過程式的程式碼自解釋的能力很差,而最好的設計就是程式碼本身。通過程式碼本身將領域中的知識呈現出來。
  4. 通過戰略設計解構複雜的業務系統,並使其簡單化。

戰術設計和戰略設計是DDD針對區域性和整體的設計指導。

貧血癥和失憶症

傳統的開發模式中,我們經常使用的是一個JavaBean,其中只有對映到資料庫的欄位,並沒有業務行為。通過填充這個JavaBean,並在物件外部進行業務邏輯的編寫,如計算訂單的最終金額填充到JavaBean中再交由資料庫對映框架進行持久化。

而其實這就是Evans所說的貧血癥,因為資料和業務行為隔離開來,形成一種無機的程式碼組成。其實這並非面向物件的編碼方式,當我們看到Order物件時,我們根本不知道它含有計算最終金額的業務邏輯,而這其實就是一種所謂的貧血癥引起的失憶症。資料和行為並沒有緊密的聯絡到一起。

public static void buyProduct(Long orderId,Double price,Long productId,Float discount) {

        Order order = new Order();

        order.setOrderId(orderId);
        order.setProductId(productId);
        order.setDiscount(discount);
        order.setPrice(price);

        // 計算最終付款金額
        if(discount == 0) {
            throw new RuntimeException("折扣不可為0!");
        }

        Double paid = price * discount;
        order.setPaid(paid);

        OrderDao.save(order);

}
複製程式碼

而更好的方式我們應該通過將資料和業務行為整合到一個物件中去,讓二者形成一種有機的程式碼組成。在GRASP物件職責中有一個原則就是當一個物件擁有某個方法所需的屬性時,那麼更應該將這個方法放置到這個物件中去,而不是放在其他地方。

 public static void buyProduct_(Long orderId,Float discount) {

        Order order = new Order(orderId,price,productId,discount);

        order.calculatePaid();

        OrderDao.save(order);

}
複製程式碼

上面的例子更加符合我們對於業務的描述,但僅僅強調將業務邏輯封裝到資料物件中去還不夠,我們還需要通過這些物件之間的協作來進行業務的表達。一個很顯而易見的例子就是關於轉賬的例子。

  public static void main(String[] args) {

        Account a = new Account(5);

        Account b = new Account(5);

        double transferMoney = 4;

        if (a.getMoney() < transferMoney) {
            throw new RuntimeException("餘額不足!");
        }

        a.setMoney(a.getMoney() - transferMoney);

        b.setMoney(b.getMoney() + transferMoney);

    }
複製程式碼

更好的做法我們應該借鑑面向物件的建模方式,將領域知識封裝到賬戶Account模型中去。

public void transfer(Account another,double transferMoney) {

        if (money < transferMoney) {
            throw new RuntimeException("餘額不足!");
        }

        money = money - transferMoney;
        another.setMoney(another.getMoney() + transferMoney);

    }
複製程式碼

A賬戶向B賬戶進行轉賬。

  public static void main(String[] args) {

        Account a = new Account(5);
        Account b = new Account(5);

        double transferMoney = 4;

        a.transfer(b,transferMoney);

    }
複製程式碼

我們通過領域模型之間的協作,呈現出來的程式碼就像白話一樣。有主語謂語賓語。主語是A賬戶,謂語是transfer()方法,賓語是B賬戶。這樣一來,程式碼的自解釋能力也就非常強了。就算產品經理不懂程式語言的話,看到我們的程式碼的話也能理解其中的業務目的。

DDD的適用場景

那什麼場景才是適合DDD的場景呢?
在可預見的未來中,專案的業務複雜度會越來越高,那就非常適合使用DDD的設計方式。而如果專案全部都是一些非常簡單的增刪查改而很少包含業務知識的話,那真是想D也D不起來,因為DDD的思想就是為了通過模型來表達領域知識,而領域知識本身就很匱乏的話,表達也就無從談起。

如何DDD

我們可以通過和領域專家(產品經理)使用一致的通用語言,利用通用語言抽象出領域模型,在根據這些領域模型進行程式碼的落地開發,這樣一來便能更好的在程式碼中去體現業務領域知識。我們需要轉變我們以往的思維慣性,少從技術層面考慮,而更應該從業務層面去考慮。我的理解是,DDD是一套基於領域為核心的面向物件程式設計的方法論。它主要通過兩種設計來實現DDD。一種是戰術設計,你可以理解為在單個微服務中的設計。一種是戰略設計,你可以理解為多個微服務之間如何進行協作。

戰術設計

戰術設計側重點在於區域性的設計,主要有以下幾個概念:

  • 實體:有唯一標識有生命週期,可以理解為通過實體可以對應到資料庫的記錄。
  • 值物件:用來描述實體的屬性。
  • 聚合:包含實體和值物件,並維護了事務一致性。
  • 資源庫:用於獲取或儲存聚合。
  • 領域服務:放置一些不適合在聚合中的業務邏輯。

我們簡單的講解一下這幾個概念是如何在單個限界上下文(即單個微服務)中進行工作的。聚合由實體、值物件進行組成,它維護了事務的一致性。而聚合又由資源庫進行持久化以及查詢或者獲取。而當一些業務規則並不能很好的放入實體或者值物件上時,我們可以使用領域服務。

戰略建模

戰略設計側重點在於整體內的不同區域性如何協作的設計,主要有以下幾個概念:

  • 限界上下文:一類領域模型運作的環境。比如電商中的商品模組即一個限界上下文。
  • 上下文對映圖:不同類的領域模型如何互動。比如在電商平臺中商品模組是如何與庫存模組進行互動的。

限界上下文是一種概念上的邊界,領域模型便工作於其中,也即一個限界上下文對應了我們設計的一個微服務。而不同上下文如何進行溝通的話,則利用上下文對映圖的概念來進行指導開發。


關於DDD的討論非常之多,每個人的見解都不一樣,這也是DDD為什麼難以流行起來的原因之一。但是DDD的思想還是非常值得借鑑的。關於DDD的學習個人非常推薦一定要閱讀原著DDD以及IDDD。

以下是筆者上傳的關於DDD和IDDD的高清PDF書籍,希望可以幫到想學習DDD的朋友們。

《領域驅動設計:軟體核心複雜性應對之道》 提取碼:6290

《實現領域驅動設計》 提取碼:xl3t

關於DDD的理解各有不同,歡迎網友評論一起探討。

轉自我的個人部落格 vc2x.com