理解ABP的領域驅動設計
大家好,我是張飛洪,感謝您的閱讀,我會不定期和你分享學習心得,希望我的文章能成為你成長路上的墊腳石,讓我們一起精進。
關于玩轉ABP框架相關的文章,之前在博客園陸續(xù)寫了《ABP vNext系列文章和視頻》,大家可以跳轉過去看,后續(xù)文章首發(fā)主要以CSDN為主。
言歸正傳,ABP 框架的主要目標是為應用程序開發(fā)引入一種架構方法,并提供必要的基礎設施和工具。
領域驅動設計(DDD) 是 ABP 產(chǎn)品架構的核心內容之一。ABP 代碼腳手架是基于 DDD 進行邏輯分層的。包括它的實體、應用服務、存儲庫、領域服務、領域事件、規(guī)約等。

由于 DDD 是 ABP 應用開發(fā)架構的核心,因此除了理論部分,我們有必要對實現(xiàn)細節(jié)進行深入分析。
一、DDD 核心概念
在我們介紹實現(xiàn)細節(jié)之前,讓我們先了解 DDD 的核心概念和構建模塊。讓我們從 DDD 的定義開始。
1.什么是領域驅動設計?
DDD 是一種針對復雜需求的軟件開發(fā)方法,它適用于復雜領域和大規(guī)模應用。對于簡單的增刪改查(CRUD),您通常不需要遵循所有 DDD 原則。但是,在復雜的應用中遵循 DDD 原則和模式可以幫助您構建靈活、模塊化和可維護的代碼庫。
DDD 關注核心領域邏輯而不是基礎設施細節(jié),這些細節(jié)通常與業(yè)務代碼隔離。
DDD 與面向對象編程(OOP) 原則密切相關。本書并未涵蓋這些基本原則,但對 OOP 和單一職責、開閉、Liskov 替換、接口隔離和依賴倒置(SOLID) 原則的良好理解仍然會對您有很大幫助。
以上提供了簡短的定義,我們先探索 DDD 的基本分層。

上圖顯示了各層及其關系:
- 領域層包含基本的業(yè)務對象,它是獨立的可重用的領域邏輯。該層不依賴于任何其他層,但所有其他層直接或間接依賴于它。
- 應用層實現(xiàn)應用操作,通常是用戶通過 UI 執(zhí)行的操作。應用層調用領域層的對象來執(zhí)行這些操作。
- 表示層包含應用的UI 組件,例如 Web 應用的視圖、JavaScript 和 CSS 文件。它不直接調用領域層或數(shù)據(jù)庫對象。相反,它調用應用層。通常,對于在 UI 上執(zhí)行的每個用例/操作,應用層都有相應的功能/方法。
- 基礎設施層依賴于所有其他層并實現(xiàn)這些層定義的抽象。它有助于優(yōu)雅地將您的業(yè)務邏輯與第三方庫和系統(tǒng)(例如數(shù)據(jù)庫或緩存提供程序)分開。
以上模型的每一層都有一個職責,并包含各種構建模塊。
1.1 DDD相關概念澄清
從技術角度來看,DDD 是主要與您的業(yè)務代碼有關。業(yè)務邏輯分為兩層——領域層和應用層。其他層(表示層和基礎設施)被視為實現(xiàn)細節(jié)。
領域層包含的概念有如下:
- 實體:實體是具有狀態(tài)(屬性)和業(yè)務邏輯的業(yè)務對象。一個實體具有一個唯一標識符 (ID),用于將該實體與其他實體區(qū)分開來。這意味著具有不同標識符的兩個實體被視為不同的實體,即使所有其他屬性都相同。
- 值對象:值對象是另一種類型的業(yè)務對象。值對象由它們的狀態(tài)(屬性)標識,它們沒有標識符 (ID)。這意味著如果兩個值對象的所有屬性都相同,則它們被認為是相同的。值對象通常比實體更簡單,并且通常不可變的。例如地址、貨幣或日期。
- 聚合和聚合根:聚合是由聚合根組織起來的一組對象(實體和值對象)集合。聚合根負責管理和協(xié)調實體對象。
- 存儲庫:存儲庫是一個類集合的接口,領域和應用層使用它來訪問持久化系統(tǒng)。它隱藏了數(shù)據(jù)庫提供者的復雜性。
- 領域服務:領域服務是實現(xiàn)核心業(yè)務規(guī)則的無狀態(tài)服務(類)。它的實現(xiàn)依賴于多種聚合(當這些聚合都不能實現(xiàn)邏輯的時候)或外部服務。
- 規(guī)約:規(guī)約是一個可重用、可測試和可組合的Lamda過濾器,用于業(yè)務規(guī)則的封裝和抽象。
- 領域事件:領域事件是一種以松散耦合的方式通知其他服務的通知。它對于連接跨多個聚合很有用。
應用層包含以下概念:
- 應用服務:應用服務是實現(xiàn)業(yè)務應用的無狀態(tài)服務(類)。它通常獲取和返回數(shù)據(jù)傳輸對象,其方法被表示層調用。它通過編排領域層對象來執(zhí)行特定的業(yè)務。業(yè)務通常表現(xiàn)為事務(原子)過程。
- 數(shù)據(jù)傳輸對象(DTO):DTO 用于在表示層和應用層之間傳輸數(shù)據(jù)(狀態(tài))。它不包含任何業(yè)務邏輯。
- 工作單元(UOW):UOW 是事務邊界。UOW 中的所有狀態(tài)更改(通常是數(shù)據(jù)庫操作)必須以原子方式實現(xiàn),成功時一起提交,失敗時一起回滾。
了解并熟悉 DDD 的核心概念很重要,這也是我在這里簡要介紹它們的原因。
2.構建基于 DDD 的 解決方案
我們已經(jīng)介紹了基于 DDD 的分層和解決方案的核心模塊。接下來我們了解如何基于 DDD 對 .NET 解決方案進行分層。先從最簡單的解決方案結構開始。然后解釋 ABP 解決方案的啟動模板是如何演變成現(xiàn)在的結構的。最后,您將了解為什么 ABP 啟動解決方案內部有這么多項目以及每個項目的用途。
2.1 創(chuàng)建一個簡單的基于 DDD 的 解決方案
讓我們從頭開始,讓我們看一下Visual Studio 中基于 DDD 的簡單 .NET 解決方案,如以下屏幕截圖所示:

假設我們正在構建客戶關系管理(CRM) 解決方案,Acme是我們的公司名稱,Crm是本示例中的產(chǎn)品名稱。我為每一層創(chuàng)建了一個單獨的 C# 項目。.NET 項目非常適合分層,因為它們可以將代碼庫物理分離到不同的包中。同一個項目中的類/類型可以相互引用。但是,跨項目則不行,除非您引用另一個項目來明確定義依賴關系。
下圖展示了項目之間依賴關系

圖中實線表示開發(fā)時依賴關系,而虛線表示運行時依賴關系。我將在本節(jié)后面解釋差異。
要理解這些依賴關系,我們需要知道這些項目可能包含什么類型的組件。
- Acme.Crm.Domain項目包含一個
Product類(聚合根實體)和一個IProductRepository接口(存儲庫抽象)。Product表示一個產(chǎn)品,并具有一些屬性,例如Id、Name和Price。IProductRepository有一些方法可以對產(chǎn)品執(zhí)行數(shù)據(jù)庫操作,例如Insert、Delete和GetList。 - Acme.Crm.Infrastructure項目包含將實體
CrmDbContext映射到數(shù)據(jù)庫表的類(EF Core 數(shù)據(jù)上下文) 。Product它還包含實現(xiàn)IproductRepository接口的EfProductRepository類。 - Acme.Crm.Application項目包含
ProductAppService(應用服務),以及一些用于增刪改查的方法。該服務在內部使用IProductRepository接口和Product實體。 - [Acme.Crm.Web]是一個 [ASP.NET] Core MVC (Razor Pages) Web 應用。它有一個
Products.cshtml頁面(和一個相關的 JS 文件),負責在 UI 上呈現(xiàn)和管理(增刪改查)產(chǎn)品。
Acme.Crm.Web項目還有一個依賴項:Acme.Crm.Infrastructure。它不直接使用該項目中的任何類,因此開發(fā)時不需要直接依賴。但是,在運行時需要基礎設施層才能使用數(shù)據(jù)庫。
以上是基于 DDD 的解決方案的簡約分層。接下來,我們將使用該解決方案并解釋 ABP 的啟動解決方案是如何演變的。
2.2 ABP啟動方案的演進
ABP 默認的啟動解決方案比上圖所示的解決方案更復雜。如下截圖:

我們從頭開始梳理,中間是怎么一步步演化過來的:
2.1.1 EntityFrameworkCore 項目介紹
簡約版的 DDD 解決方案包含Acme.Crm.Infrastructure項目,它實現(xiàn)了所有基礎設施抽象和集成。而ABP 解決方案有一個專用的基礎設施項目 Acme.Crm.EntityFrameworkCore,因為我們認為為對于數(shù)據(jù)庫集成,單獨分離出來是一種更好的設計。
當然,基礎設施層可以拆分為多個項目。目前ABP 啟動模板唯一的基礎設施項目是Acme.Crm.EntityFrameworkCore。隨著解決方案增長,您可以創(chuàng)建其他額外的基礎設施項目。
隨著這一變化,最初的基于 DDD 的極簡解決方案將如下所示:

就基礎設施層來說,目前的這種改變是微不足道的。
2.1.2 應用層介紹
Acme.Crm.Application項目包含應用服務類,Acme.Crm.Web項目通過引用Acme.Crm.Application來消費這些服務。
大家思考一下這樣引用有沒有什么問題?
這種設計有一個問題:Acme.Crm.Web間接引用了Acme.Crm.Domain(通過Acme.Crm.Application)。間接依賴具體實現(xiàn)會將領域層中的業(yè)務對象(如實體、領域服務和存儲庫)暴露給表示層,這打破了抽象和實現(xiàn)真正的分層。

所以,ABP 啟動模板將應用層分為兩個項目:
- Acme.Crm.Application.Contracts,其中包含應用服務接口(例如
IProductAppService)和相關的 DTO(例如ProductCreationDto)。 - Acme.Crm.Application,其中包含應用服務的實現(xiàn)(例如
ProductAppService)。
為應用服務引入合約(接口)有兩個優(yōu)點:
- UI 層(Acme.Crm.Web)依賴于服務契約而不依賴于實現(xiàn),因此也無需依賴于領域層。
- 可以與客戶端程序共享Acme.Crm.Application.Contracts項目,依賴相同的服務接口并重用相同的 DTO 類,而無需共享您的業(yè)務層。
官方的 EventHub 解決方案采用了這種設計,并在 UI 和 HTTP API 應用之間重用了Application.Contracts項目,通過這種方式,它可以輕松設置分層架構,其中應用層和表示層托管在不同的應用程序中,但共享服務契約接口。
分離后,當前的解決方案結構將如下圖所示:

采用這種新設計,項目依賴關系圖將如下圖所示:

Acme.Crm.Web項目現(xiàn)在只依賴于Acme.Crm.Application.Contracts項目,并且應該始終使用應用服務接口來執(zhí)行用戶交互。
目前,Acme.Crm.Web仍然依賴于Acme.Crm.Application和Acme.Crm.EntityFrameworkCore,因為我們在運行時需要它們。我用虛線繪制了這些依賴關系,以表明這些依賴關系不是最佳設計,但現(xiàn)在是必要的。
大家可以思考以下如何擺脫上面的這種依賴,實現(xiàn)更好的設計?
我們將在后面的“將宿主(Hosting)與 UI 分離”部分中介紹我們如何擺脫這些依賴。
2.1.3 領域共享項目介紹
一旦我們分離出契約,我們就不能再在契約項目中使用領域層的對象,因為它們沒有對領域層的引用。乍一看,這似乎不是問題,無論如何,我們不應該在應用服務契約中使用這些實體和其他業(yè)務對象——我們應該使用 DTO。
但是,請大家思考:假如我們仍然希望重用領域層中的某些類型或值呢?
例如,我們可能希望在 DTO 類中重用枚舉ProductType或常量值。但我們也不想從 Acme.Crm.Application.Contracts 項目中添加對Acme.Crm.Domain項目的引用。
解決方案是:引入一個新項目來存放此類類型和值。我們將這個新項目命名為Acme.Crm.Domain.Shared,因為這個項目將成為領域層的一部分并與其他項目共享。這個項目在項目中可能不會包含這么多類型,但我們仍然不想復制代碼。
隨著Acme.Crm.Domain.Shared項目的引入,新的解決方案結構如下:

下圖顯示項目之間的依賴關系:

Acme.Crm.Domain和Acme.Crm.Application.Contracts項目共享新的Acme.Crm.Domain.Shared項目。解決方案中的其他項目也都可以直接或間接地使用該新項目中的類型。
至此,ABP 啟動解決方案的基礎分層已經(jīng)完成。接下來我們繼續(xù)探討剩下的三個項目。
2.1.4 HTTP API 層介紹
ABP 啟動解決方案有兩個和HTTP 相關的項目。
第一個是Acme.Crm.HttpApi項目,包含API 控制器(即 REST API)。這個項目將 API 與 UI 分離,同時方便它們在其他場景中被重用。
第二個是Acme.Crm.HttpApi.Client,您可以使用此項目來從客戶端應用程序(可以是自己的或第三方 .NET 客戶端)使用您的 HTTP API。它使用 ABP 的動態(tài) C# 客戶端代理系統(tǒng),這個在后續(xù)會專題討論。
通過為 HTTP API 層添加兩個新項目,我們現(xiàn)在在解決方案中有八個項目,如下圖所示:

下圖顯示了添加這些新項目后的新依賴關系圖:

Acme.Crm.HttpApi和Acme.Crm.HttpApi.Client項目依賴于Acme.Crm.Application.Contracts項目,因為服務器和客戶端共享相同的契約接口。Acme.Crm.Web項目依賴于Acme.Crm.HttpApi項目,因為它在運行時提供 API。
廢棄 HTTP API 層
并非每個應用程序都需要 HTTP API(即 REST API)。在這種情況下,您甚至可以從解決方案中刪除該項目。此外,如果您愿意,可以將 API 控制器移至Acme.Crm.Web項目并丟棄Acme.Crm.HttpApi項目。
下一節(jié)將解釋解決方案中的最后一個項目。
2.1.5 了解數(shù)據(jù)庫遷移項目
上圖中,還有一個名為Acme.Crm.DbMigrator的項目。這是一個控制臺應用程序,可用于將實體遷移應用到數(shù)據(jù)庫。它是一個工具項目,而不是基本解決方案的一部分,因此無需在此處研究其詳細信息。
2.1.6 測試項目
test除了這九個項目之外,該文件夾下的解決方案中還有六個項目。它們是為每一層單獨配置的單元/集成測試項目。其中之一 (Acme.Crm.HttpApi.Client.ConsoleTestApp) 演示了如何使用Acme.Crm.HttpApi.Client調用 HTTP API。其他可以自行探索它們。
3 將宿主與 UI 分離
啟動模板的架構模型中有一件令人討厭的事情是Web項目引用了Application和EntityFramework項目。實際上,Web項目中的所有頁面/類都沒有直接使用這些項目中的類。但是,由于Web項目是運行應用程序的項目,因此我們需要引用這些項目以使它們在運行時可用。
這種結構不是什么大問題,只要你不泄露你的領域和數(shù)據(jù)庫層對象到表示(Web)層即可。
如果您擔心泄露并且不想在運行時設置開發(fā)時的依賴項,該怎么辦?
可以再添加一個項目Acme.Crm.Web.Host,如下圖所示:

通過此更改,[Acme.Crm.Web] 項目成為類庫項目,而不是最終應用程序。它僅包含應用程序的表示層頁面/組件;它不包含Startup.cs、Program.cs和appsettings.json文件。Acme.Crm.Web.Host項目通過在運行時將所有項目組合在一起來負責托管。它不包含任何應用程序 UI 頁面或組件。
我覺得這個設計更好。它從 UI 層優(yōu)雅地提取托管配置詳細信息,刪除運行時依賴項,并使其更加專注。目前,我們沒有在 ABP 啟動模板中分離托管應用程序,因為大多數(shù)開發(fā)人員已經(jīng)發(fā)現(xiàn) ABP 啟動模板很復雜。我相信讓項目職責更單一,代碼更少,比將所有東西都放在一個地方的單個項目更好。
最后總結下,在本文中,我們了解了每個項目在 ABP 啟動模板中的角色,相信您在開發(fā)解決方案時會更加自如。在下一篇中,我們將從 DDD 的角度簡要回顧 EventHub 解決方案。
浙公網(wǎng)安備 33010602011771號