從 MVC 到 DDD 的架構演進
DDD 這幾年越來越火,資料也很多,大部分的資料都偏向于理論介紹,有給出的代碼與傳統(tǒng) MVC 的三層架構差異較大,再加上大量的新概念很容易讓初學者望而卻步。本文從 MVC 架構角度來講解如何演進到 DDD 架構。
從 DDD 的角度看 MVC 架構的問題
代碼角度:
- 瘦實體模型:只起到數據類的作用,業(yè)務邏輯散落到 service,可維護性越來越差;
- 面向數據庫表編程,而非模型編程;
- 實體類之間的關系是復雜的網狀結構,成為大泥球,牽一發(fā)而動全身,導致不敢輕易改代碼;
- service 類承接的所有的業(yè)務邏輯,越來越臃腫,很容易出現幾千行的 service 類;
- 對外接口直接暴露實體模型,導致不必要開放內部邏輯對外暴露,就算有 DTO 類一般也是實體類的直接 copy;
- 外部依賴層直接從 service 層調用,字段轉換、異常處理大量充斥在 service 方法中;
項目管理角度:
- 交付效率:越來越低;
- 穩(wěn)定性差:不好測試,代碼改動的影響范圍不好預估;
- 理解成本高:新成員介入成本高,長期會導致模塊只有一個人最熟悉,離職成本很大;
第一層:初出茅廬
以上的問題越來越嚴重,很多人開始把眼光轉向 DDD,于是埋頭啃了幾本大部頭的書,對以下概念有了基本的了解:
- 統(tǒng)一語言
- 限界上下文
- 領域、子域、支撐域
- 聚合、實體、值對象
- 分層:用戶接口層、應用層、領域層、基礎層
于是把 MVC 架構進行了改造,演進成 DDD 的分層架構。
DDD 分層架構:
MVC 架構到 DDD 分層架構的映射:
至此,算了基本入門了 DDD 架構,擴展性也得到了一定的提升。不過隨著業(yè)務的發(fā)展,不斷冒出新的問題:
- 一段業(yè)務邏輯代碼,到底應該放到應用層還是領域層?
- 領域服務當成原來的 MVC 中的 service 層,隨著業(yè)務不斷發(fā)展,類也在不斷膨脹,好像還是老樣子?。?/li>
- 聚合包含多個實體類,這個接口用不到這么多實體,為了性能還是直接寫個 SQL 返回必要的操作吧,不過這樣貌似又回到了 MVC 模式
- 既然實體類可以包含業(yè)務邏輯、領域服務也可以放業(yè)務邏輯,那到底放哪里?
- 資料上說領域層不能有外部依賴,要做到 100% 單測覆蓋,可是我的領域服務中需要用到外部接口、中央緩存等等,那這不就有了外部依賴了嗎?
第二層:草船借箭(戰(zhàn)術設計)
帶著問題不斷學習他人經驗,并不斷的嘗試,逐漸 get 到以下技能:
1. 領域層
領域(domain)是個模塊,包含以下組成部分,傳統(tǒng)的 service 按功能可能拆分到任何一個地方,各司其職。
- 1 個聚合
- 1 到多個實體
- 若干值對象
- 多個 DomainService
- 1 個 Factory:新建聚合
- 1 個 Repository:聚合倉儲服務
(1) 聚合根(AggregateRoot)
聚合本身也是一個實體,聚合可以包含其他實體,其他實體不能脫離聚合而單獨提供服務,比如一篇文章下的評論,評論必須從屬與文章,沒有文章也就沒有評論。倉庫層(repository)也必須是以聚合為核心提供服務的;
- 實體:可以理解為一張數據庫表,必須有主鍵;
- 值對象:沒有主鍵,依附于實體而存在,比如用戶實體下住址對象,一般在數據庫中已 json 字符串的形式存在;最常見的值對象是枚舉;
(2) 倉庫服務(repository)
資源庫是聚合的倉儲機制,外部世界通過資源庫,而且只能通過資源庫來完成對聚合的訪問。資源庫以聚合的整體管理對象。因此,一個聚合只能有一個資源庫對象,那就是以聚合根命名的資源庫。除此之外的其他對象,都不應該提供資源庫對象。倉儲服務的實現一般有 Spring Data JPA、Mybatis 兩種方式。
如果是用 Spring Data JPA 實現,直接使用 JPA 注解 @OneToOne、@OneToMany,配合 fetch 配置,即可一個方法查詢出所有的關聯(lián)實體。
如果是用 Mybatis 實現,那么 repository 需要加入多個 mapper 的引用,再手動做拼裝。
這里有一個經典的 Hibernate 笛卡爾積問題,答案是在聚合根中,一般不會加在大量的關聯(lián)實體對象。如果確實需要查詢關聯(lián)對象而關聯(lián)對象又比較多怎么辦呢?在 DDD 中有一個 CQRS (Command-Query Responsibility Segregation) 模式,是一種讀寫分離模式,在此場景中需要將查詢操作放到查詢命令中分頁查詢。
當然 CQRS 也是一個很復雜模式,不應照搬他人方案,而是根據自己的業(yè)務場景選擇適合自己的方案,以下列舉了 CQRS 的幾種應用模式:
(3) 工廠服務(factory)
作用是創(chuàng)建聚合,只傳入必要的參數,工廠服務內部隱藏復雜的創(chuàng)建邏輯。簡單的聚合可以直接通過 new、靜態(tài)方法等創(chuàng)建,不是必須由 factory 創(chuàng)建。
(4) 領域服務
單個實體對象能處理的邏輯放到實體里,多個實體或有交互的場景放到領域服務里。
領域服務可不可以調用倉儲層或外部接口? 可以,但不能直接和領域服務代碼放一起,領域服務模塊存放 API,實現放基礎層(infrastructure)。
領域服務對象不建議直接以聚合名 + DomainService 命名,而要以操作命令關聯(lián),比如用戶保存服務命名為:UserSaveService, 審核服務:UserAuditSerivce。
2. 應用層
應用層通過應用服務接口來暴露系統(tǒng)的全部功能。在應用服務的實現中,它負責編排和轉發(fā),它將要實現的功能委托給一個或多個領域對象來實現,它本身只負責處理業(yè)務用例的執(zhí)行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的復雜性及其內部實現機制。
比如下訂單服務的方法:
public void submitOrder(Long orderId) {
Order order = OrderFetchService.fetchById(orderId); //獲取訂單對象
OrderCheckSerivce.check(order); //驗證訂單是否有效
OrderSubmitSerivce.submit(order); //提交訂單
ShoppingCartClearService.clear(order); //移除購物車中已購商品
NotifySerivce.emailNotify(order.getUser()); //發(fā)送郵件通知買家
}
對于復雜的業(yè)務來說,應用層也有幾種模式:
- 編排服務:最典型比如 Drools;
- Command、Query 命令模式;
- 業(yè)務按 Rhase、Step 逐層拆分模式;
3. Maven 模塊劃分
基礎層是比較簡單一層,不過這里還有個比較疑惑的問題:按照 DDD 的四層架構圖去劃分 Maven 模塊,基礎層是最上的一層,但是基礎層也要包含基礎組件供其他層使用,這時基礎層應該是放到最下層,直接按照這樣構建 Maven 模塊會造成循環(huán)依賴。
相比來說,另一個架構圖更準確一些,不過依然沒有直觀體現 Maven 模塊如何劃分。
我的最佳實踐是將基礎層拆分兩部分,一部分是基礎的組件 + 倉儲 API,一部分是實現,maven 模塊劃分圖如下所示:
第三層:運籌帷幄(戰(zhàn)略設計)
經過以上的兩層的磨煉,恭喜你把 DDD 戰(zhàn)術都學習完了,應付日常的代碼開發(fā)也夠了,不過作為架構師來說,探索的道路還不能止步于此,接下來會 DDD 戰(zhàn)略部分。戰(zhàn)略部分關注點有 3 個:
- 統(tǒng)一語言
- 領域
- 限界上下文
1. 統(tǒng)一語言
統(tǒng)一語言的重要性可以根據 Jeff Patton 在《用戶故事地圖》中給出的一副漫畫來直觀的描述:
統(tǒng)一語言是提煉領域知識的輸出結果,也是進行后續(xù)需求迭代及重構的基礎,統(tǒng)一語言的建立有以下幾個要點:
(1) 統(tǒng)一語言必須以文檔的形式提供出來,并且在整個項目組的各團隊達成共識;
(2) 統(tǒng)一語言必須每個中文名有對應的英文名,并且在整個技術棧保持一致;
(3) 統(tǒng)一語言必須是完整的,包含以下要素:
- 領域模型的概念與邏輯;
- 界限上下文(Bounded Context);
- 系統(tǒng)隱喻;
- 職責的分層;
- 模式(patterns)與慣用法。
2. 領域劃分
以事件風暴的形式(Event Storming),列出所有的用戶故事(Use Story),用戶故事可通過 6W 模型來構建,即描寫場景的 Who、What、Why、Where、When 與 hoW 六個要素。然后圈選功能相近的部分,就形成了領域,領域又根據職能不同劃分為:核心域、支撐域、通用域,
具體的過程有很多參考資料,這里不在細講,最終的輸出是領域劃分圖,以下是一個保險業(yè)務示例:
3. 限界上下文
限界上下文包含兩部分:上下文(Context)是業(yè)務目標,限界(Bounded)則是保護和隔離上下文的邊界。
比如上圖中的實現部分即是限界上下文的邊界,虛線部分代表了領域的邊界。限界上下文沒有統(tǒng)一的劃分標準,需要的讀者根據自己的業(yè)務場景來甄別如何劃分。
一個上下文中包含了相同的領域知識,角色在上下文中完成動作目標;
邊界體現在以下幾方面:
- 領域邏輯層:確定了領域模型的業(yè)務邊界,維護了模型的完整性與一致性,從而降低系統(tǒng)的業(yè)務復雜度;
- 團隊合作層:限界上下文一般也是用戶換分團隊的依據;
- 技術實現層:限界上下文可當成是微服務的劃分邊界;
DDD 的不足
DDD 架構作為一套先進的方法論,在很多場景能發(fā)揮很大價值,但是 DDD 也不是銀彈。高級的架構師把 DDD 架構當成一種工具,結合其他架構經驗一起為業(yè)務服務。
DDD 的不足有幾個方面:
- 性能:DDD 是基于聚合來組織代碼,對于高性能場景下,加載聚合中大量的無用字段會嚴重影響性能,比如報表場景中,直接寫 SQL 會更簡答直接;
- 事務:DDD 中的事務被限定在限界上下文中,跨多個限界上下文的場景需要開發(fā)者額外考慮分布式事務問題;
- 難度系數高,推廣成本大:DDD 項目需要領域專家專家,且需要特別熟悉業(yè)務、建模、OOP,對于管理者來說評估一個人是否真的能勝任也是一件困難的事情;
總結
本文從 MVC 架構開始講述了如何從演進到 DDD 架構,限于篇幅很多 DDD 的知識點沒有講到,希望大家在實踐過程中能靈活運用,盡享 DDD 給業(yè)務帶來的價值。