由Spring應用的瑕疵談談DDD的概念與應用(二)

上一篇文章中,經過Spring Web應用的瑕疵引出改善的措施,咱們講解了領域驅動開發的相關概念和設計策略。本文主要講解領域模型的幾種類型和DDD的簡單實踐案例。html

架構風格

在《實現領域驅動設計》一書中提到了幾種架構風格:六邊形架構、REST架構、CQRS 和事件驅動等。在實際使用中,落地的架構並不是是純粹其中的一種,而頗有可能戶將上述幾種架構風格結合起來實現。java

分層架構

分層架構的一個重要原則是每層只能與位於其下方的層發生耦合。分層架構能夠簡單分爲兩種,即嚴格分層架構和鬆散分層架構。在嚴格分層架構中,某層只能與位於其直接下方的層發生耦合,而在鬆散分層架構中,則容許某層與它的任意下方層發生耦合。DDD分層架構中比較經典的三種模式:四層架構、五層架構和六邊形架構。程序員

四層架構

Eric Evans在《領域驅動設計-軟件核心複雜性應對之道》這本書中提出了傳統的四層架構模式:web

  • User Interface爲用戶界面層(或表示層),負責向用戶顯示信息和解釋用戶命令。這裏指的用戶能夠是另外一個計算機系統,不必定是使用用戶界面的人。
  • Application爲應用層,定義軟件要完成的任務,而且指揮表達領域概念的對象來解決問題。這一層所負責的工做對業務來講意義重大,也是與其它系統的應用層進行交互的必要渠道。應用層要儘可能簡單,不包含業務規則或者知識,而只爲下一層中的領域對象協調任務,分配工做,使它們互相協做。它沒有反映業務狀況的狀態,可是卻能夠具備另一種狀態,爲用戶或程序顯示某個任務的進度。
  • Domain爲領域層(或模型層),負責表達業務概念,業務狀態信息以及業務規則。儘管保存業務狀態的技術細節是由基礎設施層實現的,可是反映業務狀況的狀態是由本層控制而且使用的。領域層是業務軟件的核心,領域模型位於這一層。
  • Infrastructure層爲基礎實施層,向其餘層提供通用的技術能力:爲應用層傳遞消息,爲領域層提供持久化機制,爲用戶界面層繪製屏幕組件,等等。基礎設施層還可以經過架構框架來支持四個層次間的交互模式。

傳統的四層架構都是限定型鬆散分層架構,即Infrastructure層的任意上層均可以訪問該層(「L」型),而其它層遵照嚴格分層架構。算法

五層架構

五層架構是根據《DCI架構:面向對象編程的新構想》中說起的DCI架構模式總結而成。DCI架構(Data、Context和Interactive三層架構):編程

  • Data層描述系統有哪些領域概念及其之間的關係,該層專一於領域對象的確立和這些對象的生命週期管理及關係,讓程序員站在對象的角度思考系統,從而讓「系統是什麼」更容易被理解。
  • Context層:是儘量薄的一層。Context每每被實現得無狀態,只是找到合適的role,讓role交互起來完成業務邏輯便可。可是簡單並不表明不重要,顯示化context層正是爲人去理解軟件業務流程提供切入點和主線。
  • Interactive層主要體如今對role的建模,role是每一個context中複雜的業務邏輯的真正執行者,體現「系統作什麼」。role所作的是對行爲進行建模,它聯接了context和領域對象。因爲系統的行爲是複雜且多變的,role使得系統將穩定的領域模型層和多變的系統行爲層進行了分離,由role專一於對系統行爲進行建模。該層每每關注於系統的可擴展性,更加貼近於軟件工程實踐,在面向對象中更多的是以類的視角進行思考設計。

DCI目前普遍被看做是對DDD的一種發展和補充,用在基於面向對象的領域建模上。五層架構的具體定義以下:設計模式

  • User Interface是用戶接口層,主要用於處理用戶發送的Restful請求和解析用戶輸入的配置文件等,並將信息傳遞給Application層的接口。
  • Application層是應用層,負責多進程管理及調度、多線程管理及調度、多協程調度和維護業務實例的狀態模型。當調度層收到用戶接口層的請求後,委託Context層與本次業務相關的上下文進行處理。
  • Context是環境層,以上下文爲單位,將Domain層的領域對象cast成合適的role,讓role交互起來完成業務邏輯。
  • Domain層是領域層,定義領域模型,不只包括領域對象及其之間關係的建模,還包括對象的角色role的顯式建模。
  • Infrastructure層是基礎實施層,爲其餘層提供通用的技術能力:業務平臺,編程框架,持久化機制,消息機制,第三方庫的封裝,通用算法,等等。
六邊形架構

六邊形架構(Hexagonal Architecture),又稱爲端口和適配器風格,最先由 Alistair Cockburn 提出。在 DDD 社區獲得了發展和推廣,之因此是六變形是爲了突顯這是個扁平的架構,每一個邊界的權重是相等的。ruby

咱們知道,經典分層架構分爲三層(展示層、應用層、數據訪問層),而對於六邊形架構,能夠分紅另外的三層:網絡

  • 領域層(Domain Layer):最裏面,純粹的核心業務邏輯,通常不包含任何技術實現或引用。
  • 端口層(Ports Layer):領域層以外,負責接收與用例相關的全部請求,這些請求負責在領域層中協調工做。端口層在端口內部做爲領域層的邊界,在端口外部則扮演了外部實體的角色。
  • 適配器層(Adapters Layer):端口層以外,負責以某種格式接收輸入、及產生輸出。好比,對於 HTTP 用戶請求,適配器會將轉換爲對領域層的調用,並將領域層傳回的響應進行封送,經過 HTTP 傳回調用客戶端。在適配器層不存在領域邏輯,它的惟一職責就是在外部世界與領域層之間進行技術性的轉換。適配器可以與端口的某個協議相關聯並使用該端口,多個適配器可使用同一個端口,在切換到某種新的用戶界面時,可讓新界面與老界面同時使用相同的端口。

圖片轉自網絡
圖片轉自網絡

這樣作的好處是將使業務邊界更加清晰,從而得到更好的擴展性,除此以外,業務複雜度和技術複雜度分離,是 DDD 的重要基礎,核心的領域層能夠專一在業務邏輯而不用理會技術依賴,外部接口在被消費者調用的時候也不用去關心業務內部是如何實現。多線程

REST架構

RESTful風格的架構將 資源 放在第一位,每一個 資源 都有一個 URI 與之對應,能夠將 資源 看着是 DDD 中的實體;RESTful 採用具備自描述功能的消息實現無狀態通訊,提升系統的可用性;至於 資源 的哪些屬性能夠公開出去,針對 資源的操做,RESTful使用HTTP協議的已有方法來實現:GET、PUT、POST和DELETE。

在 DDD 的實現中,咱們能夠將對外的服務設計爲 RESTful 風格的服務,將實體/值對象/領域服務做爲資源對外提供增刪改查服務。可是並不建議直接將實體暴露在外,一來實體的某些隱私屬性並不能對外暴露,二來某些資源獲取場景並非一個實體就能知足。所以咱們在實際實踐過程當中,在領域模型上增長了 DTO 這樣一個角色,DTO 能夠組合多個實體/值對象的資源對外暴露。

CQRS

CQRS 就是日常你們在講的讀寫分離,一般讀寫分離的目的是爲了提升查詢性能,同時達到讀/寫的解耦。讓 DDD 和 CQRS 結合,咱們能夠分別對讀和寫建模,查詢模型一般是一種非規範化數據模型,它並不反映領域行爲,只是用於數據顯示;命令模型執行領域行爲,且在領域行爲執行完成後,想辦法通知到查詢模型。

那麼命令模型如何通知到查詢模型呢? 若是查詢模型和領域模型共享數據源,則能夠省卻這一步;若是沒有共用數據源,則能夠藉助於 消息模式(Messaging Patterns)通知到查詢模型,從而達到最終一致性(Eventual Consistency)。

Martin 在 blog 中指出:CQRS 適用於極少數複雜的業務領域,若是不是很適合反而會增長複雜度;另外一個適用場景爲獲取高性能的服務。

領域模型

在上面小節講解了領域驅動設計的幾種架構風格,下面咱們具體結合簡單的實例來看其中的領域模型劃分,初步分爲4大類:

  1. 失血模型
  2. 貧血模型
  3. 充血模型
  4. 脹血模型

咱們看看這些領域模型的具體內容,以及他們的優缺點。

失血模型

失血模型簡單來講,就是domain object只有屬性的getter/setter方法的純數據類,全部的業務邏輯徹底由business object來完成(又稱TransactionScript),這種模型下的domain object被Martin Fowler稱之爲「貧血的domain object」。以下:

  • 一個實體類叫作Item

    public class Item implements Serializable {   
     private Long id = null;   
     private int version;   
     private String name;   
     private User seller;   
     // ... 
     // getter/setter方法省略不寫,避免篇幅太長 
    複製代碼

}
```

  • 一個DAO接口類叫作ItemDao

    public interface ItemDao {   
     public Item getItemById(Long id);   
     public Collection findAll();   
     public void updateItem(Item item);   
    複製代碼

} ```

  • 一個DAO接口實現類叫作ItemDaoHibernateImpl

    public class ItemDaoImpl implements ItemDao extends DaoSupport {   
     public Item getItemById(Long id) {   
         return (Item) getHibernateTemplate().load(Item.class, id);   
     }   
     public Collection findAll() {   
         return (List) getHibernateTemplate().find("from Item");   
     }   
     public void updateItem(Item item) {   
         getHibernateTemplate().update(item);   
     }   
    複製代碼

} ```

  • 一個業務邏輯類叫作ItemManager(或者叫作ItemService)

    複製代碼

public class ItemManager {
private ItemDao itemDao;
public void setItemDao(ItemDao itemDao) { this.itemDao = itemDao;}
public Bid loadItemById(Long id) {
itemDao.loadItemById(id);
}
public Collection listAllItems() {
return itemDao.findAll();
}
public Bid placeBid(Item item, User bidder, MonetaryAmount bidAmount,
Bid currentMaxBid, Bid currentMinBid) throws BusinessException {
if (currentMaxBid != null && currentMaxBid.getAmount().compareTo(bidAmount) > 0) {
throw new BusinessException("Bid too low.");
}

// ...  
複製代碼

} ```

以上是一個完整的失血模型的示例代碼。在這個示例中,loadItemById、findAll 等等業務邏輯通通放在 ItemManager 中實現,而 Item 只有 getter/setter 方法。

貧血模型

簡單來講,就是 domain ojbect 包含了不依賴於持久化的領域邏輯,而那些依賴持久化的領域邏輯被分離到 Service 層。

Service(業務邏輯,事務封裝) --> DAO ---> domain object

這也就是 Martin Fowler 指的 rich domain object:

  • 一個帶有業務邏輯的實體類,即domain object是Item
  • 一個DAO接口ItemDao
  • 一個DAO實現ItemDaoHibernateImpl
  • 一個業務邏輯對象ItemManager

這種模型的優勢:

  1. 各層單向依賴,結構清楚,易於實現和維護
  2. 設計簡單易行,底層模型很是穩定

缺點爲:

  1. domain object的部分比較緊密依賴的持久化 domain logic 被分離到Service層,顯得不夠 OO
  2. Service 層過於厚重

具體代碼較爲簡單,再也不展現。

充血模型

充血模型和第二種模型差很少,所不一樣的就是如何劃分業務邏輯,即認爲,絕大多業務邏輯都應該被放在domain object裏面(包括持久化邏輯),而Service層應該是很薄的一層,僅僅封裝事務和少許邏輯,不和DAO層打交道。

Service(事務封裝) ---> domain object <---> DAO

這種模型就是把第二種模型的 domain object 和 business object 合二爲一了。因此 ItemManager 就不須要了,在這種模型下面,只有三個類,他們分別是:

  • Item:包含了實體類信息,也包含了全部的業務邏輯
  • ItemDao:持久化DAO接口類
  • ItemDaoHibernateImpl:DAO接口的實現類

在這種模型中,全部的業務邏輯所有都在Item中,事務管理也在Item中實現。 這種模型的優勢:

  1. 更加符合OO的原則
  2. Service層很薄,只充當Facade的角色,不和DAO打交道。

這種模型的缺點:

  1. DAO和domain object造成了雙向依賴,複雜的雙向依賴會致使不少潛在的問題。
  2. 如何劃分Service層邏輯和domain層邏輯是很是含混的,在實際項目中,因爲設計和開發人員的水平差別,可能致使整個結構的混亂無序。
  3. 考慮到Service層的事務封裝特性,Service層必須對全部的domain object的邏輯提供相應的事務封裝方法,其結果就是Service徹底重定義一遍全部的domain logic,很是煩瑣,並且 Service 的事務化封裝其意義就等於把 OO 的domain logic 轉換爲過程的 Service TransactionScript。

脹血模型

基於充血模型的第三個缺點,有同窗提出,乾脆取消Service層,只剩下domain object和DAO兩層,在domain object的domain logic上面封裝事務。

domain object(事務封裝,業務邏輯) <---> DAO

彷佛ruby on rails就是這種模型,他甚至把 domain object 和 DAO 都合併了。

該模型優勢:

  1. 簡化了分層
  2. 也算符合OO

該模型缺點:

  1. 不少不是domain logic的 service 邏輯也被強行放入 domain object,引發了domain ojbect模型的不穩定
  2. domain object 暴露給web層過多的信息,可能引發意想不到的反作用。

小結

在這四種模型當中,失血模型和脹血模型應該是不被提倡的。而貧血模型和充血模型從技術上來講,都已是可行的了。貧血模型和充血模型哪一個更加好一些?人們針對這個問題進行了曠日持久的爭論,最後仍然沒有什麼結果。雙方爭論的焦點主要在我上面加粗的兩句話上,就是領域模型是否要依賴持久層,由於依賴持久層就意味着單元測試的展開要更加困難(沒法脫離框架進行測試,原文的討論中這裏專指Hibernate),領域層就更難獨立,未來也更難從應用程序中剝離出來,固然好處是業務邏輯沒必要混放在不一樣的層中,使得單一職責性體現的更好。而支持者(充血模型)認爲,只要將持久層抽象出來,便可減小測試的困難性,同時適用充血模型畢竟帶來了很多開發上的便利性,除了依賴持久層這一點,擁有更多好處的充血模型仍然值得選擇。最後,誰也沒能說服誰,關於貧血模型和充血模型的選擇,更多的要靠具體的業務場景來決定,並不能說哪種更比哪種好。設計模式這種東西不是向來都沒有什麼定論麼。

我我的則傾向使用充血模型,由於充血模型更加像一個設計完善的系統架構,好在計算機世界裏有不少的 IOC 和 DI 框架,惟一的缺陷依賴持久層能夠經過各類變通的方法繞過,隨着技術的進步,一些缺陷也會被慢慢解決。個人思路是這樣的:先將持久層抽象爲接口,而後經過服務層將持久層注入到領域模型中,這樣領域模型僅僅會依賴於持久層的接口。而這個接口,能夠利用現有框架的技術進行抽象。

參考

  1. 【DDD】業務建模實踐 —— 刪除帖子
  2. 貧血,充血模型的解釋以及一些經驗
相關文章
相關標籤/搜索