新項目,不妨採用這種架構分層,很優雅!

大家好,我是飄渺。今天繼續更新DDD&微服務的系列文章。

在專欄開篇提到過DDD(Domain-Driven Design,領域驅動設計)學習起來較爲複雜,一方面因爲其自身涉及的概念頗多,另一方面,我們往往缺乏實戰經驗和明確的代碼模型指導。今天,我們將專注於DDD的分層架構和實體模型,期望爲大家落地DDD提供一些有益的參考。首先,讓我們回顧一下熟悉的MVC三層架構。

1. MVC 架構

在傳統應用程序中,我們通常採用經典的MVC(Model-View-Controller)架構進行開發,它將整體的系統分成了 Model(模型),View(視圖)和 Controller(控制器)三個層次,也就是將用戶視圖和業務處理隔離開,並且通過控制器連接起來,很好地實現了表現和邏輯的解耦,是一種標準的軟件分層架構。

在遵循此分層架構的開發過程中,我們通常會建立三個Maven Module:Controller、Service 和 Dao,它們分別對應表現層、邏輯層和數據訪問層,如下圖所示:

image-20230602123152660

(圖中多畫了一個Model層是因爲 Model 通常只是簡單的 Java Bean,只包含數據庫表對應的屬性。有的應用會將其單獨抽取出來作爲一個Maven Module,但實際上它可以合併到 DAO 層。)

1.1 MVC架構模型的不足

在業務邏輯較爲簡單的應用中,MVC三層架構是一種簡潔高效的開發模式。然而,隨着業務邏輯的複雜性增加和代碼量的增加,MVC架構可能會顯得捉襟見肘。其主要的不足可以總結如下:

  • Service層職責過重:在MVC架構中,Service層常常被賦予處理複雜業務邏輯的任務。隨着業務邏輯的增長,Service層可能變得臃腫和複雜。業務邏輯有可能分散在各個Service類中,使得業務邏輯的組織和維護成爲一項挑戰。
  • 過於關注數據庫而忽視領域建模:雖然MVC的設計初衷是對數據、用戶界面和控制邏輯進行分離,但它在面對複雜業務場景時並未給予領域建模足夠的重視。這可能導致代碼難以理解和擴展,因爲代碼更像是圍繞數據庫而不是業務需求進行設計。
  • 邊界劃分不明確:在MVC架構中,頂層設計上的邊界劃分並沒有明確的規則,往往依賴於技術負責人的經驗。在大規模的團隊協作中,這可能導致職責不清晰、分工不明確等問題。
  • 單元測試困難:在MVC架構中,Service層通常以事務腳本的方式進行開發,並且往往耦合了各種中間件操作,如數據庫、緩存、消息隊列等。這種耦合使得單元測試變得困難,因爲要在沒有這些中間件的情況下運行測試可能需要大量的模擬或存根代碼。

在深入探討MVC架構之後,我們將進入今天的主題:DDD的分層架構模型。

2. DDD的架構模型

在DDD中,通常將應用程序分爲四個層次,分別爲用戶接口層(Interface Layer)應用層(Application Layer)領域層(Domain Layer)基礎設施層(Infrastructure Layer),每個層次承擔着各自的職責和作用。分層模型如下圖所示:

image.png

  1. 接口層(Interface Layer):負責處理與外部系統的交互,包括UI、Web API、RPC接口等。它會接收用戶或外部系統的請求,然後調用應用層的服務來處理這些請求,最後將處理結果返回給用戶或外部系統。
  2. 應用層(Application Layer):承擔協調領域層和基礎設施層的職責,實現具體的業務邏輯。它調用領域層的領域服務和基礎設施層的基礎服務,完成業務邏輯的實現。
  3. 領域層(Domain Layer):該層包含了業務領域的所有元素,如實體、值對象、領域服務、聚合、工廠和領域事件等。這一層的主要職責是實現業務領域的核心邏輯。
  4. 基礎設施層(Infrastructure Layer):主要提供通用的技術能力,如數據持久化、緩存、消息傳輸等基礎設施服務。它可被其他三層調用,提供各種必要的技術服務。

在這四層中,調用關係通常是單向依賴的,即上層依賴下層,下層並不依賴上層。例如,接口層依賴應用層,應用層依賴領域層,領域層依賴基礎設施層。但值得注意的是,儘管基礎設施層在物理結構上可能位於最底層,但在DDD的分層模型中,它位於最外層,爲內部各層提供技術服務。

image-20230604220949124

2.1 依賴反轉原則

依賴反轉原則(Dependency Inversion Principle, DIP)是一種有效的設計原則,有助於減小模塊間的耦合度,提高系統的擴展性和可維護性。依賴反轉原則的核心思想是:高層模塊不應直接依賴低層模塊,它們都應該依賴抽象。抽象不應該依賴具體的實現,而具體的實現應當依賴於抽象。

在 DDD 的四層架構中,領域層是核心,是業務的抽象化,不應直接依賴其他任何層。這意味着領域層的業務對象應該與其他層(如基礎設施層)解耦,而不是直接依賴於具體的數據庫訪問技術、消息隊列技術等。但在實際運行時,領域層的對象需要通過基礎設施層來實現數據的持久化、消息的發送等。

爲了解決這個問題,我們可以使用依賴翻轉原則。在領域層,我們定義一些接口(如倉儲接口),用於聲明領域對象需要的服務,具體的實現則由基礎設施層完成。在基礎設施層,我們實現這些接口,並將實現類注入到領域層的對象中。這樣,領域層的對象就可以通過這些接口與基礎設施層進行交互,而不需要直接依賴於基礎設施層。

2.2 DDD四層架構的優勢

在複雜的業務場景下,採用DDD的四層架構模型可以有效地解決使用MVC架構可能出現的問題:

  1. 職責分離:在DDD的設計中,我們嘗試將業務邏輯封裝到領域對象(如實體、值對象和領域服務)中。這樣可以降低應用層(原MVC中的Service層)的複雜性,同時使得業務邏輯更加集中和清晰,易於維護和擴展。
  2. 領域建模:DDD的核心理念在於通過建立富有內涵的領域模型來更真實地反映業務需求和業務規則,從而提高代碼的靈活性,使其更容易適應業務的變化。
  3. 明確的邊界劃分:DDD通過邊界上下文(Bounded Context)的概念,對系統進行明確的邊界劃分。每個邊界上下文都有自己的領域模型和業務邏輯,使得大規模團隊協作更加清晰、高效。
  4. 易於測試:由於業務邏輯封裝在領域對象中,我們可以直接對這些領域對象進行單元測試。同時,基礎設施層(如數據庫、緩存和消息隊列)被抽象爲接口,我們可以使用模擬對象(Mock Object)進行測試,避免了直接與真實中間件的交互,大大提升了測試的靈活性和便利性。

接下來看看如何在代碼中遵循DDD的分層架構。

3. 如何實現DDD分層架構

爲了遵循DDD的分層架構,在代碼實現時有兩種實現方法。

第一種是在模塊中通過包進行隔離,即在模塊中建立4個不同的代碼包,分別對應領域層(Domain Layer)、應用層(Application Layer)、基礎設施層(Infrastructure Layer)和用戶接口層(User Interface Layer)。這種方法的優點是結構簡單,易於理解和維護。但缺點是各層之間的依賴關係可能不夠明確,容易導致代碼耦合。

image.png

第二種實現方法是建立4個不同的Maven Module層,每個Module分別對應領域層、應用層、基礎設施層和用戶接口層。這種方法的優點是各層之間的依賴關係更加明確,有利於降低耦合度和提高代碼的可重用性。同時,這種方法也有助於團隊成員更好地理解和遵循DDD的分層架構。然而,這種方法可能會導致項目結構變得複雜,增加了項目的維護成本。

image.png

在實際項目中,可以根據項目規模、團隊成員的熟悉程度以及項目需求來選擇合適的實現方法。對於較小規模的項目,可以採用第一種方法,通過包進行隔離。而對於較大規模的項目,建議採用第二種方法,使用Maven Module層進行隔離,以便更好地管理和維護代碼。無論採用哪種方法,關鍵在於確保各層之間的職責分明,遵循DDD的原則和最佳實踐。

在DailyMart項目中,我最初打算採用第一種方法,通過包進行隔離。然而,在微信羣中進行投票後,發現近90%的人選擇了第二種方法。作爲一個傾聽粉絲意見的博主,我決定採納大家的建議。因此,DailyMart將採用Maven Module層隔離的方式進行編碼實踐。
image.png

4. DDD中的數據模型

在DDD中,我們採用特定的模型來映射和處理不同的領域概念和責任,常見的有三種數據模型:實體對象(Entity)、數據對象(Data Object,DO)和數據傳輸對象(Data Transfer Object,DTO)。這些模型在DDD中有着明確的角色和使用場景:

  • Entity(實體對象): 實體對象代表業務領域中的核心概念,其字段和方法應與業務語言保持一致,與持久化方式無關。這意味着實體和數據對象可能具有完全不同的字段命名、字段類型,甚至嵌套關係。實體的生命週期應僅存在於內存中,無需可序列化和可持久化。
  • Data Object (DO、數據對象): DO可能是我們在日常工作中最常見的數據模型。在DDD規範中,數據對象不能包含業務邏輯,並且位於基礎設施層,僅負責與數據庫進行交互,通常與數據庫的物理表一一對應。
  • DTO(數據傳輸對象): 數據傳輸對象主要用作接口層和應用層之間傳遞數據,例如CQRS模式中的命令(Command)、查詢(Query)、事件(Event)以及請求(Request)和響應(Response)。DTO的重要性在於它能夠適配不同的業務場景需要的參數,從而避免業務對象變成龐大而複雜的"萬能"對象。

在DDD中,這三種數據對象在很多場景下需要相互轉換,例如:

  1. Entity <-> DTO:在應用層返回數據時,需要將實體對象轉換成DTO,這一般通過一個名爲DTO Assembler的轉換器來完成。

  2. Entity <-> DO:在基礎設施層的Repository實現時,我們需要將實體轉換爲DO以存儲到數據庫。同樣地,查詢數據時需要將DO轉換回實體。這通常通過一個名爲Data Converter的轉換器來完成。

當然,不管是Entity轉DTO,還是Entity轉DO,都會有一定的開銷,無論是代碼量還是運行時的操作來看。手寫轉換代碼容易出錯,而使用反射技術雖然可以減少代碼量,但可能會導致顯著的性能損耗。這裏給用Java的同學推薦MapStruct這個庫,MapStruct在編譯時生成代碼,只需通過接口定義和註解配置就能生成相應的代碼。由於生成的代碼是直接賦值,所以性能損耗可以忽略不計。

image.png

在SpringBoot老鳥系列中我推薦大家使用 Orika 進行對象轉換,理由是隻需要編寫少量代碼。但是在DDD中不同對象都有嚴格的代碼層級,並且一般會引入專門的Assembler和Converter轉換器,既然代碼量省不了,必然要選擇性能最高的組件。

各種轉換器的性能對比:Performance of Java Mapping Frameworks | Baeldung

5. 小結

本篇文章詳細介紹了DDD的分層架構,並詳細解釋瞭如何在項目代碼中實現這種分層架構。同時,還詳細DDD中三種常用的數據對象:數據對象(DO)、實體(Entity)和數據傳輸對象(DTO)。這三種數據對象的區別可以通過下圖進行精煉總結:

image-20230523220725247

至此,我們已經深入解析了DDD中的核心概念。同時,我們的DailyMart商城系統已完成所有的前期準備,現在已經準備好進入實際的編碼階段。在接下來的章節中,我們將從實現註冊流程開始,逐步探索如何在實際項目中應用DDD。

最後,歡迎關注公衆號 Java日知錄 ,獲取最新的文章和源碼更新。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章