什麼是領域驅動設計?
領域驅動設計(簡稱:DDD)是一種針對複雜需求的軟體開發方法。將軟體實現與不斷發展的模型聯絡起來,專注於核心領域邏輯,而不是基礎設施細節。DDD適用於複雜領域和大規模應用,而不是簡單的CRUD應用。它有助於建立一個靈活、模組化和可維護的程式碼庫。
OOP 和 SOLID
DDD實現高度依賴物件導向程式設計思想(OOP)和SOLID原則。實際上,實現並擴充套件了這些原則。因此,在真正實施DDD時,對OOP和SOLID的良好理解將對您有很大幫助。
DDD 和 Clean Architecture
一個基於領域驅動的解決方案有四個基本層:
業務邏輯分佈在兩個層中:領域層(Domain Layer)和 應用層(Application Layer),分別包含不同型別的業務邏輯:
- 領域層:實現領域(或系統)中的用例獨立的核心業務邏輯。
- 應用層:基於領域的應用程式用例,應用程式用例可以看作是使用者介面上的使用者互動。
- 展示層:包含應用程式UI元素(頁面、元件等)。
- 基礎層:支援層,通過對第三方類庫的呼叫或系統的抽象和整合來實現對其他層的支援。
簡潔架構(Clean Architecture) 是與之相同的分層架構,又稱為洋蔥架構(Onion Architecture)。
從架構圖可以看出,每一層只直接依賴於它內部的層,最獨立的層是領域層,顯示在最內圈中。
核心構件
DDD主要關注領域層和應用層,展示層和基礎層被看作是細節,業務層不應該依賴於它們,但這並不意味著展示層和基礎層不重要,它們也非常重要。展示層中的UI框架和基礎層中的資料提供程式有他們自己的實現規則和最佳實踐,需要了解和應用。然而,這些並不在DDD的主題中,我們重點來看領域層和應用層的基本構件。
領域層構件
- 實體(Entity):一個實體是一個物件,該物件包含自己的屬性和方法,屬性用於儲存資料和描述狀態;方法結合屬性實現業務邏輯。一個實體使用唯一標識(ID)來表示,兩個實體物件ID不同則是為不同的實體。
- 值物件(Value Object):值物件是另一種型別的領域物件,該物件由其屬性而不是唯一ID來標識。意思是說,只有全部屬性相同才會被認為是同一個物件。值物件通常被實現為不可變的,而且大多比實體簡單得多。
- 聚合和聚合根:聚合根是一個特定型別的實體,具有額外的職責。聚合是以聚合根為中心繫結在一起的一組物件,物件包括實體和值物件。
- 倉儲(介面):倉儲是一個類似集合的介面,被領域層和應用層用來訪問資料持久化系統(資料庫)。它將資料庫的複雜性從業務程式碼中隱藏起來。領域層包含倉儲介面。
- 領域服務:領域服務是無狀態服務,實現核心領域業務規則。用於實現依賴於多個聚合(實體)或外部服務的領域邏輯。
- 規約:用於為實體和其他業務物件定義可命名的、可重用的和可組合的過濾器。
- 領域事件:領域事件是一種低耦合的通知方式,當一個特定的領域事件發生時,會通知其他服務。
應用層構件
- 應用服務:應用服務是無狀態服務,實現應用程式用例。一個應用服務通常獲取和返回資料傳輸物件(DTOs),用於展示層。呼叫領域物件來實現用例。一個用例通常被認為是一個工作單元。
- 資料傳輸物件(DTO):DTO是簡單物件,不包含任何業務邏輯,只用於在應用層和展示層傳遞資料。
- 工作單元:一個工作單元是一個原子工作。在工作單元中的所有操作統一提交,要麼全部成功,失敗則全部回滾。
實現:全景圖
專案分層
下圖是在 .Net解決方案(Visual Studio),基於 ABP 應用程式啟動模板建立的解決方案結構:
解決方案名稱為:IssueTracking
。解決方案的專案分層考慮到DDD原則,同時兼顧開發和部署實踐而劃分。
示例專案業務場景參考 GitHub 問題追蹤,這個場景比較通用,使用過Git的開發人員都瞭解。
領域層
領域層拆分為兩個專案:
- IssueTracking.Domain:領域層,該專案包含所有領域層構件,比如:實體、值物件、領域服務、規約、倉儲介面等。
- IssueTracking.Domain.Shared:領域共享層,包含屬於領域層,但是與其他層共享的型別。舉個例子:定義的常量和列舉,既在領域物件中使用,也要在其他層中使用,放在該專案中。
應用層
應用層拆分為兩個專案:
- IssueTracking.Application.Contracts:應用契約層,包含應用服務介面和資料傳輸物件(用於介面),該專案被應用程式客戶端引用,比如:WEB專案、API客戶端專案。
- IssueTracking.Application:應用層,實現在 Contracts 專案中定義的介面。
展示層
- IssueTracking.Web:可執行程式,呼叫應用服務或APIs,當前解決方案中是 ASP.NET Core MVC/Razor Pages 應用。
ABP框架提供不同型別的UI框架,比如:Angular和Blazor。如果採用這種UI框架,解決方案為前後端分離架構,解決方案中不包含 IssueTracking.Web 專案,而是通過 IssueTracking.HttpApi.Host 專案作為一個獨立的端點提供 HTTP API 服務,供客戶端呼叫。
遠端服務層
- IssueTracking.HttpApi:遠端服務層,該專案用於定義 HTTP APIs,通常包含 MVC Controller 及相關的模型。
大多數時候,API Controller 只是應用服務的包裝器,以便將它們公開給遠端客戶端。因為ABP框架提供根據應用服務介面自動生成API Controller,實現自動配置並將你的應用服務公開為API控制器,所以通常不會在這個專案中建立控制器。
- IssueTracking.HttpApi.Client:遠端服務代理層,客戶端應用程式引用該專案,將直接通過依賴注入使用遠端應用服務,該專案基於ABP Framework動態C#客戶端API代理系統實現。在C#專案中需要呼叫HTTP APIs時,會非常有用。
在解決方案的
test
資料夾中有一個控制檯應用程式,名為IssueTracking.HttpApi.Client.ConsoleTestApp
。它只是使用IssueTracking.HttpApi.Client
專案來消費應用程式所暴露的API。它只是一個演示應用程式,可以安全地刪除它。如果認為不需要,甚至可以刪除IssueTracking.HttpApi.Client
專案。
基礎層
實現DDD時,可以使用一個基礎層專案來實現所有的整合和抽象,當然也可以為不同依賴建立不同專案。
建議折中處理,為核心基礎依賴建立單獨專案,比如:Entity Framework Core;另外建立一個公共基礎專案存放其他基礎設施。
啟動模板中包含兩個專案對 Entity Framework Core 進行整合:
- IssueTracking.EntityFrameworkCore:EF Core核心基礎依賴專案,包含:資料上下文、資料庫對映、EF Core倉儲實現等。
- IssueTracking.EntityFrameworkCore.DbMigrations:資料遷移專案,是一個特殊的工具專案,用於管理 Code First 資料遷移。專案中有獨立的資料上下文,用於資料遷移。除了在需要建立新的資料庫遷移或新增應用程式模組增加相應的表時,需要建立一個新的資料庫遷移之外,通常不會涉及這個專案。
可能你會疑惑為什麼整合EF Core建立了兩個專案,因為模組化的需要。每一個模組有其獨立的
DbContext
,應用程式也有一個DbContext
。DbMigration
專案包含用於跟蹤和應用單個遷移模組的聯合。雖然大多數時候您不需要了解它,但您可以檢視 EF Core遷移文件,以獲得更多資訊。
其他專案
還有一個專案,IssueTracking.DbMigrator
,一個簡單的控制檯應用程式,當你執行它時,會遷移資料庫結構並初始化種子資料。這是一個有用的實用程式,可以在開發和生產環境中使用它。
專案依賴關係
下圖是解決方案中專案引用(依賴)關係
前面我們講解了各個專案的作用,接下來梳理專案之前的關係:
Domain.Shared
其他專案直接或間接引用,專案中定義的型別在所有專案中共享。Domain
只引用Domain.Shared
,比如:在Domain.Shared
中定義的IssuType
列舉型別需要在Domain
專案中Issue
實體中用到。Application.Contracts
依賴Domain.Shared
,這樣我們可以在 DTOs 中使用這些共享型別。比如:CreateIssueDto
中可以直接使用IssueType
列舉。Application
依賴Application.Contracts
,因為Application
實現Application.Contracts
中定義的服務介面和使用 DTO 物件。同時,引用Domain
專案,在應用服務中使用倉儲介面或領域物件。EntiryFrameworkCore
依賴Domain
,對映Domain
物件(實體和值型別)到資料庫表(ORM)並實現在Domain
中定義的倉儲介面。HttpApi
依賴Application.Contract
,在控制器在內部對 應用服務介面 進行依賴注入。HttpApi.Client
依賴Application.Contract
消費應用服務Web
依賴HttpApi
,釋出裡面定義的HTTP APIs
。另外,通過這種方式,它間接地依賴於Application.Contracts
專案,可以在頁面/元件中使用應用服務。
虛擬依賴
當你仔細檢視解決方案依賴關係圖時,會看到還有兩個依賴關係,在上圖中用虛線表示。Web
專案依賴於 Application
和 EntityFrameworkCore
專案,理論上不應該是這樣,但實際上是這樣。
這是因為 Web
是執行和託管應用程式的最終專案,應用程式在執行時需要應用服務和倉儲的實現。
這個設計決定有可能讓你在展示層中使用實體和EF Core 物件,但這應該是嚴格避免的。然而,我們發現替代設計過於複雜。在這裡,如果你想消除這種依賴性,有兩個備選方案:
- 將 Web 專案轉換為 Razor類庫型別,然後建立新專案,比如:Web.Host,引用 Web 專案、Application 和 EntityFrameworkCore 專案。在新專案中,不需要編寫任何UI程式碼,只用來做承載專案。
- 從 Web 專案中移除 Application 和 EntityFrameworkCore 專案引用,作為 ABP 外掛模組在應用初始化時載入程式集。
DDD應用程式的執行流程
下圖顯示基於DDD模式開發的Web應用請求的基本流程:
- 通過UI使用者互動(可以看做是一個用例)發起HTTP請求到伺服器
- 在展示層 MVC Controller(HTTP API) 或 Razor Page Handler(Razor Pages)接收並處理請求,在此階段執行橫切關注點,如:授權、輸入驗證、異常處理、審計日誌、快取等。Controller或Page在建構函式中注入應用服務介面,呼叫方法傳送和接收DTO物件。
- 應用服務使用領域物件(實體、倉儲介面、領域服務等)實現用例。在此階段,應用層執行橫切關注點,如:授權、驗證、審計日誌、工作單元等。一個應用服務方法是一個工作單元,具有原子性。
大多數橫切關注點在ABP框架中自動實現或按照約定實現,無需額外編寫程式碼。
通用原則
在進入DDD之前,讓我們梳理下DDD通用原則。
資料庫(Database Provider / ORM)獨立性原則
領域層和應用層不知道專案中使用的 ORM 和 Database Provider。只依賴於倉儲介面,並且倉儲介面不適合使用用任何 ORM 特殊物件。
這一原則的主要原因是:
- 使領域層和應用層與基礎層獨立,因為基礎層將來可能更改,或者你可能需要支援其他型別資料庫。
- 使領域和應用聚焦在業務程式碼上,通過將基礎設施實現細節隱藏於倉儲之後,使您的領域和應用服務專注於業務程式碼。
- 易於自動化測試,因為可以通過倉儲介面模擬倉儲資料。
根據這一原則,除啟動應用程式外,解決方案中的任何專案都沒有引用
EntityFrameworkCore
專案。
關於資料庫獨立性原則的討論
尤其是原因1會深深地影響你的領域物件設計(比如,實體關係)和應用層程式碼。假設你當前使用 Entity Framework Core 操作關係型資料庫,後期希望切換為 MongoDB,這就決定你不能使用 EF Core 中獨有功能,因為在MongoDB中不被支援。
舉個例子:
- 不能使用更改跟蹤(Change Tacking),因為 MongoDB 不支援。所以,需要顯式更改實體。
- 不能在實體中使用導航屬性(Navigation Properties) 或集合關聯其他聚合,因為可能在文件資料庫中不支援。
那麼如何解決實體關聯的問題?記住規則:僅通過Id引用其他聚合。
如果你認為這些功能對你很重要,而且你永遠不會棄用 EF Core,我們認為這個原則是可以有彈性的,但是我們仍然建議使用倉儲模式來隱藏基礎設施的實現細節。
ABP Framework 為倉儲介面
IRepository
提供獲取IQueryable
物件的擴充套件方法GetQueryableAsync()
,使我們在使用倉儲時可以直接使用標準LINQ擴充套件方法。
展示技術無關性原則
展示層技術(UI框架)是應用程式中變化最多的部分,將領域層和應用層設計成完全不知道展示層技術或框架是非常重要的。
這一原則相對容易實現,而ABP的啟動模板使其更加容易實現,選擇不同UI框架自動生成對應的啟動模板專案。
在某些場景下,你可能需要在應用層和展示層使用相同的邏輯。舉例,你可能需要在兩個層中進行驗證和授權。在UI層檢測是為了提高使用者體驗,在應用層和領域層是出安全和資料有效性考慮。這是非常正常和必要的。
聚焦狀態變化,而不是效能優化
DDD聚焦領域物件如何變化和如何互動;如何建立實體和改變屬性,並且保持資料的完整性、有效性;如何建立方法,實現業務規則。
DDD沒有考慮報表和大規模查詢等需要高效能的業務場景,如果你的應用程式中沒有花哨的儀表盤或報表功能,誰會去考慮呢?意思是我們需要自己考慮效能問題。
效能優化或技術選型,只要不影響到業務邏輯,可以自由使用 SQL Server 全部功能,比如:查詢優化、索引、儲存過程等技術;甚至使用一個其他資料來源,如:ElasticSearch,來負責報表功能。
學習幫助
圍繞DDD和ABP Framework兩個核心技術,後面還會陸續釋出核心構件實現、綜合案例實現系列文章,敬請關注!
ABP Framework 研習社(QQ群:726299208)
ABP Framework 學習及實施DDD經驗分享;示例原始碼、電子書共享,歡迎加入!