基於ABP落地領域驅動設計-01.全景圖

什麼是領域驅動設計?

領域驅動設計(簡稱:DDD)是一種針對複雜需求的軟體開發方法。將軟體實現與不斷發展的模型聯繫起來,專註於核心領域邏輯,而不是基礎設施細節。DDD適用於複雜領域大規模應用,而不是簡單的CRUD應用。它有助於建立一個靈活、模組化和可維護的程式碼庫。

OOP 和 SOLID

DDD實現高度依賴面向對象編程思想(OOP)SOLID原則。實際上,實現並擴展了這些原則。因此,在真正實施DDD時,對OOP和SOLID的良好理解將對您有很大幫助。

DDD 和 Clean Architecture

一個基於領域驅動的解決方案有四個基本層:

image

業務邏輯分布在兩個層中:領域層(Domain Layer)和 應用層(Application Layer),分別包含不同類型的業務邏輯:

  • 領域層:實現領域(或系統)中的用例獨立的核心業務邏輯。
  • 應用層:基於領域的應用程式用例,應用程式用例可以看作是用戶介面上的用戶交互。
  • 展示層:包含應用程式UI元素(頁面、組件等)。
  • 基礎層:支援層,通過對第三方類庫的調用或系統的抽象和集成來實現對其他層的支援。

簡潔架構(Clean Architecture) 是與之相同的分層架構,又稱為洋蔥架構(Onion Architecture)。

image

從架構圖可以看出,每一層只直接依賴於它內部的層,最獨立的層是領域層,顯示在最內圈中。

核心構件

DDD主要關注領域層和應用層,展示層和基礎層被看作是細節,業務層不應該依賴於它們,但這並不意味著展示層和基礎層不重要,它們也非常重要。展示層中的UI框架和基礎層中的數據提供程式有他們自己的實現規則和最佳實踐,需要了解和應用。然而,這些並不在DDD的主題中,我們重點來看領域層和應用層的基本構件。

領域層構件

  • 實體(Entity):一個實體是一個對象,該對象包含自己的屬性和方法,屬性用於存儲數據和描述狀態;方法結合屬性實現業務邏輯。一個實體使用唯一標識(ID)來表示,兩個實體對象ID不同則是為不同的實體。
  • 值對象(Value Object):值對象是另一種類型的領域對象,該對象由其屬性而不是唯一ID來標識。意思是說,只有全部屬性相同才會被認為是同一個對象。值對象通常被實現為不可變的,而且大多比實體簡單得多。
  • 聚合和聚合根:聚合根是一個特定類型的實體,具有額外的職責。聚合是以聚合根為中心綁定在一起的一組對象,對象包括實體和值對象。
  • 倉儲(介面):倉儲是一個類似集合的介面,被領域層和應用層用來訪問數據持久化系統(資料庫)。它將資料庫的複雜性從業務程式碼中隱藏起來。領域層包含倉儲介面。
  • 領域服務:領域服務是無狀態服務,實現核心領域業務規則。用於實現依賴於多個聚合(實體)或外部服務的領域邏輯。
  • 規約:用於為實體和其他業務對象定義可命名的、可重用的和可組合的過濾器。
  • 領域事件:領域事件是一種低耦合的通知方式,當一個特定的領域事件發生時,會通知其他服務。

應用層構件

  • 應用服務:應用服務是無狀態服務,實現應用程式用例。一個應用服務通常獲取和返回數據傳輸對象(DTOs),用於展示層。調用領域對象來實現用例。一個用例通常被認為是一個工作單元。
  • 數據傳輸對象(DTO):DTO是簡單對象,不包含任何業務邏輯,只用於在應用層和展示層傳遞數據。
  • 工作單元:一個工作單元是一個原子工作。在工作單元中的所有操作統一提交,要麼全部成功,失敗則全部回滾。

實現:全景圖

項目分層

下圖是在 .Net解決方案(Visual Studio),基於 ABP 應用程式啟動模板創建的解決方案結構:

image

解決方案名稱為: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,應用程式也有一個 DbContextDbMigration項目包含用於跟蹤和應用單個遷移模組的聯合。雖然大多數時候您不需要了解它,但您可以查看 EF Core遷移文檔,以獲得更多資訊。

其他項目

還有一個項目,IssueTracking.DbMigrator,一個簡單的控制台應用程式,當你執行它時,會遷移資料庫結構並初始化種子數據。這是一個有用的實用程式,可以在開發和生產環境中使用它。

項目依賴關係

下圖是解決方案中項目引用(依賴)關係

image

前面我們講解了各個項目的作用,接下來梳理項目之前的關係:

  • 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項目依賴於 ApplicationEntityFrameworkCore 項目,理論上不應該是這樣,但實際上是這樣。

這是因為 Web 是運行和託管應用程式的最終項目,應用程式在運行時需要應用服務和倉儲的實現

這個設計決定有可能讓你在展示層中使用實體EF Core 對象,但這應該是嚴格避免的。然而,我們發現替代設計過於複雜。在這裡,如果你想消除這種依賴性,有兩個備選方案:

  • 將 Web 項目轉換為 Razor類庫類型,然後創建新項目,比如:Web.Host,引用 Web 項目、Application 和 EntityFrameworkCore 項目。在新項目中,不需要編寫任何UI程式碼,只用來做承載項目。
  • 從 Web 項目中移除 Application 和 EntityFrameworkCore 項目引用,作為 ABP 插件模組在應用初始化時載入程式集。

DDD應用程式的執行流程

下圖顯示基於DDD模式開發的Web應用請求的基本流程:

image

  • 通過UI用戶交互(可以看做是一個用例)發起HTTP請求到伺服器
  • 在展示層 MVC Controller(HTTP API) 或 Razor Page Handler(Razor Pages)接收並處理請求,在此階段執行橫切關注點,如:授權、輸入驗證、異常處理、審計日誌、快取等。Controller或Page在構造函數中注入應用服務介面,調用方法發送和接收DTO對象。
  • 應用服務使用領域對象(實體、倉儲介面、領域服務等)實現用例。在此階段,應用層執行橫切關注點,如:授權、驗證、審計日誌、工作單元等。一個應用服務方法是一個工作單元,具有原子性。

大多數橫切關注點在ABP框架中自動實現或按照約定實現,無需額外編寫程式碼。

通用原則

在進入DDD之前,讓我們梳理下DDD通用原則。

資料庫(Database Provider / ORM)獨立性原則

領域層和應用層不知道項目中使用的 ORM 和 Database Provider。只依賴於倉儲介面,並且倉儲介面不適合使用用任何 ORM 特殊對象

這一原則的主要原因是:

  1. 使領域層和應用層與基礎層獨立,因為基礎層將來可能更改,或者你可能需要支援其他類型資料庫。
  2. 使領域和應用聚焦在業務程式碼上,通過將基礎設施實現細節隱藏於倉儲之後,使您的領域和應用服務專註於業務程式碼。
  3. 易於自動化測試,因為可以通過倉儲介面模擬倉儲數據。

根據這一原則,除啟動應用程式外,解決方案中的任何項目都沒有引用 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,來負責報表功能。

學習幫助

圍繞DDDABP Framework兩個核心技術,後面還會陸續發布核心構件實現綜合案例實現系列文章,敬請關注!

ABP Framework 研習社(QQ群:726299208)
ABP Framework 學習及實施DDD經驗分享;示例源碼、電子書共享,歡迎加入!
image