DDD這幾年越來越火,資料也很多,大部分的資料都偏向於理論介紹,有給出的程式碼與傳統MVC的三層架構差異較大,再加上大量的新概念很容易讓初學者望而卻步。本文從MVC架構角度來講解如何演進到DDD架構。
從DDD的角度看MVC架構的問題
程式碼角度:
- 瘦實體模型:只起到資料類的作用,業務邏輯散落到service,可維護性越來越差;
- 面向資料庫表程式設計,而非模型程式設計;
- 實體類之間的關係是複雜的網狀結構,成為大泥球,牽一髮而動全身,導致不敢輕易改程式碼;
- service類承接的所有的業務邏輯,越來越臃腫,很容易出現幾千行的service類;
- 對外介面直接暴露實體模型,導致不必要開放內部邏輯對外暴露,就算有DTO類一般也是實體類的直接copy;
- 外部依賴層直接從service層呼叫,欄位轉換、異常處理大量充斥在service方法中;
專案管理角度:
- 交付效率:越來越低;
- 穩定性差:不好測試,程式碼改動的影響範圍不好預估;
- 理解成本高:新成員介入成本高,長期會導致模組只有一個人最熟悉,離職成本很大;
第一層:初出茅廬
以上的問題越來越嚴重,很多人開始把眼光轉向DDD,於是埋頭啃了幾本大部頭的書,對以下概念有了基本的瞭解:
- 統一語言
- 限界上下文
- 領域、子域、支撐域
- 聚合、實體、值物件
- 分層:使用者介面層、應用層、領域層、基礎層
於是把MVC架構進行了改造,演進成DDD的分層架構。
DDD分層架構:
MVC架構到DDD分層架構的對映:
至此,算了基本入門了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的幾種應用模式:
工廠服務(factory)
作用是建立聚合,只傳入必要的引數,工廠服務內部隱藏複雜的建立邏輯。簡單的聚合可以直接通過new、靜態方法等建立,不是必須由factory建立。
領域服務
單個實體物件能處理的邏輯放到實體裡,多個實體或有互動的場景放到領域服務裡。
領域服務可不可以呼叫倉儲層或外部介面? 可以,但不能直接和領域服務程式碼放一起,領域服務模組存放API,實現放基礎層(infrastructure)。
領域服務物件不建議直接以聚合名+DomainService命名,而要以操作命令關聯,比如使用者儲存服務命名為:UserSaveService, 稽核服務:UserAuditSerivce。
2、應用層
應用層通過應用服務介面來暴露系統的全部功能。在應用服務的實現中,它負責編排和轉發,它將要實現的功能委託給一個或多個領域物件來實現,它本身只負責處理業務用例的執行順序以及結果的拼裝。通過這樣一種方式,它隱藏了領域層的複雜性及其內部實現機制。
比如下訂單服務的方法:
- public void submitOrder(Long orderId) {
- Order order = OrderFetchService.fetchById(orderId); //獲取訂單物件
- OrderCheckSerivce.check(order); //驗證訂單是否有效
- OrderSubmitSerivce.submit(order); //提交訂單
- ShoppingCartClearService.clear(order); //移除購物車中已購商品
- NotifySerivce.emailNotify(order.getUser()); //傳送郵件通知買家
- }
對於複雜的業務來說,應用層也有幾種模式:
- 編排服務:最典型比如Drools;
- Command、Query命令模式;
- 業務按Rhase、Step逐層拆分模式;
3、Maven模組劃分
基礎層是比較簡單一層,不過這裡還有個比較疑惑的問題:按照DDD的四層架構圖去劃分Maven模組,基礎層是最上的一層,但是基礎層也要包含基礎元件供其他層使用,這時基礎層應該是放到最下層,直接按照這樣構建Maven模組會造成迴圈依賴。
相比來說,另一個架構圖更準確一些,不過依然沒有直觀體現Maven模組如何劃分。
我的最佳實踐是將基礎層拆分兩部分,一部分是基礎的元件+倉儲API,一部分是實現,maven模組劃分圖如下所示:
第三層:運籌帷幄(戰略設計)
經過以上的兩層的磨鍊,恭喜你把DDD戰術都學習完了,應付日常的程式碼開發也夠了,不過作為架構師來說,探索的道路還不能止步於此,接下來會DDD戰略部分。戰略部分關注點有3個:
- 統一語言
- 領域
- 限界上下文
1、統一語言
統一語言的重要性可以根據Jeff Patton 在《使用者故事地圖》中給出的一副漫畫來直觀的描述:
統一語言是提煉領域知識的輸出結果,也是進行後續需求迭代及重構的基礎,統一語言的建立有以下幾個要點:
- 統一語言必須以文件的形式提供出來,並且在整個專案組的各團隊達成共識;
- 統一語言必須每個中文名有對應的英文名,並且在整個技術棧保持一致;
- 統一語言必須是完整的,包含以下要素:
- 領域模型的概念與邏輯;
- 界限上下文(Bounded Context);
- 系統隱喻;
- 職責的分層;
- 模式(patterns)與慣用法。
2、領域劃分
以事件風暴的形式(Event Storming),列出所有的使用者故事(Use Story),使用者故事可通過6W模型來構建,即描寫場景的 Who、What、Why、Where、When 與 hoW 六個要素。然後圈選功能相近的部分,就形成了領域,領域又根據職能不同劃分為:核心域、支撐域、通用域,
具體的過程有很多參考資料,這裡不再細講,最終的輸出是領域劃分圖,以下是一個保險業務示例:
3、限界上下文
限界上下文包含兩部分:上下文(Context)是業務目標,限界(Bounded)則是保護和隔離上下文的邊界。
比如上圖中的實現部分即是限界上下文的邊界,虛線部分代表了領域的邊界。限界上下文沒有統一的劃分標準,需要的讀者根據自己的業務場景來甄別如何劃分。
一個上下文中包含了相同的領域知識,角色在上下文中完成動作目標;
邊界體現在以下幾方面:
- 領域邏輯層:確定了領域模型的業務邊界,維護了模型的完整性與一致性,從而降低系統的業務複雜度;
- 團隊合作層:限界上下文一般也是使用者換分團隊的依據;
- 技術實現層:限界上下文可當成是微服務的劃分邊界;
DDD的不足
DDD架構作為一套先進的方法論,在很多場景能發揮很大價值,但是DDD也不是銀彈。高階的架構師把DDD架構當成一種工具,結合其他架構經驗一起為業務服務。
DDD的不足有幾個方面:
- 效能:DDD是基於聚合來組織程式碼,對於高效能場景下,載入聚合中大量的無用欄位會嚴重影響效能,比如報表場景中,直接寫SQL會更簡單直接;
- 事務:DDD中的事務被限定在限界上下文中,跨多個限界上下文的場景需要開發者額外考慮分散式事務問題;
- 難度係數高,推廣成本大:DDD專案需要領域專家專家,且需要特別熟悉業務、建模、OOP,對於管理者來說評估一個人是否真的能勝任也是一件困難的事情;
總結
本文從MVC架構開始講述瞭如何從演進到DDD架構,限於篇幅很多DDD的知識點沒有講到,希望大家在實踐過程中能靈活運用,盡享DDD給業務帶來的價值。本文如有不足之處敬請反饋。
本文連結:從MVC到DDD的架構演進
作者簡介:木小豐,美團Java技術專家,專注分享軟體研發實踐、架構思考。歡迎關注公共號:Java研發