從MVC到DDD的架構演進

木小豐發表於2022-02-15

DDD這幾年越來越火,資料也很多,大部分的資料都偏向於理論介紹,有給出的程式碼與傳統MVC的三層架構差異較大,再加上大量的新概念很容易讓初學者望而卻步。本文從MVC架構角度來講解如何演進到DDD架構。

從DDD的角度看MVC架構的問題

程式碼角度:

  • 瘦實體模型:只起到資料類的作用,業務邏輯散落到service,可維護性越來越差;
  • 面向資料庫表程式設計,而非模型程式設計;
  • 實體類之間的關係是複雜的網狀結構,成為大泥球,牽一髮而動全身,導致不敢輕易改程式碼;
  • service類承接的所有的業務邏輯,越來越臃腫,很容易出現幾千行的service類;
  • 對外介面直接暴露實體模型,導致不必要開放內部邏輯對外暴露,就算有DTO類一般也是實體類的直接copy;
  • 外部依賴層直接從service層呼叫,欄位轉換、異常處理大量充斥在service方法中;

專案管理角度:

  • 交付效率:越來越低;
  • 穩定性差:不好測試,程式碼改動的影響範圍不好預估;
  • 理解成本高:新成員介入成本高,長期會導致模組只有一個人最熟悉,離職成本很大;

第一層:初出茅廬

以上的問題越來越嚴重,很多人開始把眼光轉向DDD,於是埋頭啃了幾本大部頭的書,對以下概念有了基本的瞭解:

  • 統一語言
  • 限界上下文
  • 領域、子域、支撐域
  • 聚合、實體、值物件
  • 分層:使用者介面層、應用層、領域層、基礎層

於是把MVC架構進行了改造,演進成DDD的分層架構。

DDD分層架構:

image

image

 

MVC架構到DDD分層架構的對映:

image

image

 

至此,算了基本入門了DDD架構,擴充套件性也得到了一定的提升。不過隨著業務的發展,不斷冒出新的問題:

  • 一段業務邏輯程式碼,到底應該放到應用層還是領域層?
  • 領域服務當成原來的MVC中的service層,隨著業務不斷髮展,類也在不斷膨脹,好像還是老樣子啊?
  • 聚合包含多個實體類,這個介面用不到這麼多實體,為了效能還是直接寫個SQL返回必要的操作吧,不過這樣貌似又回到了MVC模式
  • 既然實體類可以包含業務邏輯、領域服務也可以放業務邏輯,那到底放哪裡?
  • 資料上說領域層不能有外部依賴,要做到100%單測覆蓋,可是我的領域服務中需要用到外部介面、中央快取等等,那這不就有了外部依賴了嗎?

第二層:草船借箭(戰術設計)

帶著問題不斷學習他人經驗,並不斷的嘗試,逐漸get到以下技能:

1、領域層

領域(domain)是個模組,包含以下組成部分,傳統的service按功能可能拆分到任何一個地方,各司其職。

  • 1個聚合
  • 1到多個實體
  • 若干值物件
  • 多個DomainService
  • 1個Factory:新建聚合
  • 1個Repository:聚合倉儲服務
聚合根(AggregateRoot)

聚合本身也是一個實體,聚合可以包含其他實體,其他實體不能脫離聚合而單獨提供服務,比如一篇文章下的評論,評論必須從屬於文章,沒有文章也就沒有評論。倉庫層(repository)也必須是以聚合為核心提供服務的;

實體:可以理解為一張資料庫表,必須有主鍵;

值物件:沒有主鍵,依附於實體而存在,比如使用者實體下住址物件,一般在資料庫中已json字串的形式存在;最常見的值物件是列舉;

倉庫服務(repository)

資源庫是聚合的倉儲機制,外部世界通過資源庫,而且只能通過資源庫來完成對聚合的訪問。資源庫以聚合的整體管理物件。因此,一個聚合只能有一個資源庫物件,那就是以聚合根命名的資源庫。除此之外的其他物件,都不應該提供資源庫物件。倉儲服務的實現一般有Spring Data JPA、Mybatis兩種方式。

如果是用Spring Data JPA實現,直接使用JPA註解@OneToOne、@OneToMany,配合fetch配置,即可一個方法查詢出所有的關聯實體。

如果是用Mybatis實現,那麼repository需要加入多個mapper的引用,再手動做拼裝。

這裡有一個經典的Hibernate笛卡爾積問題,答案是在聚合根中,一般不會加在大量的關聯實體物件。如果確實需要查詢關聯物件而關聯物件又比較多怎麼辦呢?在DDD中有一個CQRS(Command-Query Responsibility Segregation)模式,是一種讀寫分離模式,在此場景中需要將查詢操作放到查詢命令中分頁查詢。

當然CQRS也是一個很複雜模式,不應照搬他人方案,而是根據自己的業務場景選擇適合自己的方案,以下列舉了CQRS的幾種應用模式:

image

image

 

工廠服務(factory)

作用是建立聚合,只傳入必要的引數,工廠服務內部隱藏複雜的建立邏輯。簡單的聚合可以直接通過new、靜態方法等建立,不是必須由factory建立。

領域服務

單個實體物件能處理的邏輯放到實體裡,多個實體或有互動的場景放到領域服務裡。

領域服務可不可以呼叫倉儲層或外部介面? 可以,但不能直接和領域服務程式碼放一起,領域服務模組存放API,實現放基礎層(infrastructure)。

領域服務物件不建議直接以聚合名+DomainService命名,而要以操作命令關聯,比如使用者儲存服務命名為:UserSaveService, 稽核服務:UserAuditSerivce。

2、應用層

應用層通過應用服務介面來暴露系統的全部功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委託給一個或多個領域物件來實現,它本身只負責處理業務用例的執行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的複雜性及其內部實現機制。

比如下訂單服務的方法:

  1. public void submitOrder(Long orderId) {
  2. Order order = OrderFetchService.fetchById(orderId); //獲取訂單物件
  3. OrderCheckSerivce.check(order); //驗證訂單是否有效
  4. OrderSubmitSerivce.submit(order); //提交訂單
  5. ShoppingCartClearService.clear(order); //移除購物車中已購商品
  6. NotifySerivce.emailNotify(order.getUser()); //傳送郵件通知買家
  7. }

對於複雜的業務來說,應用層也有幾種模式:

  • 編排服務:最典型比如Drools;
  • Command、Query命令模式;
  • 業務按Rhase、Step逐層拆分模式;

image

image

 

3、Maven模組劃分

基礎層是比較簡單一層,不過這裡還有個比較疑惑的問題:按照DDD的四層架構圖去劃分Maven模組,基礎層是最上的一層,但是基礎層也要包含基礎元件供其他層使用,這時基礎層應該是放到最下層,直接按照這樣構建Maven模組會造成迴圈依賴。

image

image

 

相比來說,另一個架構圖更準確一些,不過依然沒有直觀體現Maven模組如何劃分。

image

image

 

我的最佳實踐是將基礎層拆分兩部分,一部分是基礎的元件+倉儲API,一部分是實現,maven模組劃分圖如下所示:

image

image

 

第三層:運籌帷幄(戰略設計)

經過以上的兩層的磨鍊,恭喜你把DDD戰術都學習完了,應付日常的程式碼開發也夠了,不過作為架構師來說,探索的道路還不能止步於此,接下來會DDD戰略部分。戰略部分關注點有3個:

  • 統一語言
  • 領域
  • 限界上下文
1、統一語言

統一語言的重要性可以根據Jeff Patton 在《使用者故事地圖》中給出的一副漫畫來直觀的描述:

image

image

 

統一語言是提煉領域知識的輸出結果,也是進行後續需求迭代及重構的基礎,統一語言的建立有以下幾個要點:

  • 統一語言必須以文件的形式提供出來,並且在整個專案組的各團隊達成共識;
  • 統一語言必須每個中文名有對應的英文名,並且在整個技術棧保持一致;
  • 統一語言必須是完整的,包含以下要素:
    1. 領域模型的概念與邏輯;
    2. 界限上下文(Bounded Context);
    3. 系統隱喻;
    4. 職責的分層;
    5. 模式(patterns)與慣用法。
2、領域劃分

以事件風暴的形式(Event Storming),列出所有的使用者故事(Use Story),使用者故事可通過6W模型來構建,即描寫場景的 Who、What、Why、Where、When 與 hoW 六個要素。然後圈選功能相近的部分,就形成了領域,領域又根據職能不同劃分為:核心域、支撐域、通用域,

具體的過程有很多參考資料,這裡不再細講,最終的輸出是領域劃分圖,以下是一個保險業務示例:

image

image

 

3、限界上下文

限界上下文包含兩部分:上下文(Context)是業務目標,限界(Bounded)則是保護和隔離上下文的邊界。

比如上圖中的實現部分即是限界上下文的邊界,虛線部分代表了領域的邊界。限界上下文沒有統一的劃分標準,需要的讀者根據自己的業務場景來甄別如何劃分。

一個上下文中包含了相同的領域知識,角色在上下文中完成動作目標;

邊界體現在以下幾方面:

  • 領域邏輯層:確定了領域模型的業務邊界,維護了模型的完整性與一致性,從而降低系統的業務複雜度;
  • 團隊合作層:限界上下文一般也是使用者換分團隊的依據;
  • 技術實現層:限界上下文可當成是微服務的劃分邊界;

DDD的不足

DDD架構作為一套先進的方法論,在很多場景能發揮很大價值,但是DDD也不是銀彈。高階的架構師把DDD架構當成一種工具,結合其他架構經驗一起為業務服務。

DDD的不足有幾個方面:

  1. 效能:DDD是基於聚合來組織程式碼,對於高效能場景下,載入聚合中大量的無用欄位會嚴重影響效能,比如報表場景中,直接寫SQL會更簡單直接;
  2. 事務:DDD中的事務被限定在限界上下文中,跨多個限界上下文的場景需要開發者額外考慮分散式事務問題;
  3. 難度係數高,推廣成本大:DDD專案需要領域專家專家,且需要特別熟悉業務、建模、OOP,對於管理者來說評估一個人是否真的能勝任也是一件困難的事情;

總結

本文從MVC架構開始講述瞭如何從演進到DDD架構,限於篇幅很多DDD的知識點沒有講到,希望大家在實踐過程中能靈活運用,盡享DDD給業務帶來的價值。本文如有不足之處敬請反饋。

本文連結:從MVC到DDD的架構演進

作者簡介:木小豐,美團Java技術專家,專注分享軟體研發實踐、架構思考。歡迎關注公共號:Java研發

相關文章