架構之經典分層
上一篇:《IDDD 實現領域驅動設計-上下文映射圖及其相關概念》
在《實現領域驅動設計》書中,分層的概念作者講述的很少,也就幾頁的內容,但對於我來說,有很多的感觸需要訴說。之前的短消息項目使用的就是經典分層架構,但那時候是:瞎子過橋,啥也不會,現在再回過頭看,滿眼惆悵,還請我娓娓道來~
1. 層的含義
在第一張圖中,用戶界面層(User Layer)是我自作主張加上的,應用層的直接用戶就是用戶界面層,這里的用戶界面層,也可以稱之為表現層(Presentation Layer),上面箭頭表示依賴關系,第二張是現在短消息項目的解決方案圖(不是很完善),這兩個圖結合起來可以更加容易理解。
分層架構是所有架構的鼻祖,分層的作用就是隔離,不過,我們有時候有個誤解,就是把層和程序集對應起來,就比如簡單三層架構中,在你的解決方案中,一般會有三個程序集項目:UI.dll、BLL.dll 和 DAL.dll,然后把這三個程序集看成一個層,這沒什么不可以,但當項目復雜的時候,如果還按照這種方式的話,你的程序集中的文件夾會越來越多,程序集也會越來越大。當你的視野跳出這個程序集的概念后,你會發現,層不只是和程序集對應,也和解決方案文件夾,或者是整個解決方案對應,一個層甚至可以對應一個系統,這個在之前的領域概念中可以對應理解,比如身份與訪問通用子域,在不同的場景中,可以是一個獨立的系統,也可以是項目中的一個通用組件。
關於層的概念,我再多說一點,因為之前了解過領域和限界上下文的概念,所以有些感觸。首先,在開發人員眼里,一個業務系統的分層只是技術架構上的,所以,我們會把日志紀錄、權限管理、數據庫持久化、消息服務等等,把一些能分離出來的盡量分離出來,然后再把這些東西組合起來,我們一般稱之為基礎設施層,或者是系統幫助層,它們貫徹於整個業務系統,這些工作做完后,我們就會沿着“三層架構”的思想,再次進行分層,首先搭建 Web 層,然后是 BLL 和 DAL,可能名字有些差別(BLL 變成了 Application,DAL 變成了 Dao),解決方案中的項目可能很多(其實都是分離出來的),但如果你仔細分離項目,你會發現,其實還是三層架構,只不過在它基礎之上,做了一些調整和完善。這時候,你看了一下自己的項目架構,然后覺得我是在胡說,我舉一個例子,比如 Web 中一個簡單的獲取數據展示,調用 BLL 中的一個 GetDataById 方法,這個 BLL 對象,在 Web 層是通過 IoC 容器獲取到的,所以 Web 只依賴於 IBLL,而不依賴 IBLL 的具體實現,然后你再看一下 BLL 中的 GetDataById 方法(名字一般不變),里面一般會有一些緩存處理、通知處理、日志處理、對象轉換(DTO 映射)等等,但很少有一些業務處理,然后再調用 DAL 中的 GetDataById 方法,和 Web 層一樣,也是通過 IoC 獲取 IDAL 的對象,DAL 中的 GetDataById 方法實現,可以是 ADO.NET,也可以是 ORM,但看一下實現代碼,你會發現你的真正業務一般會隱藏在這里面。
上面我說的是一個方法的調用過程,其他的也是類似,我說這么多是什么意思呢?就是你的思路會局限在一個解決方案中,或者是一個項目中,並且分層的概念也只是在一個解決方案,比如一個用戶模塊,這個在多個項目中一般會是通用的,而不必在每個項目中進行獨立實現,還有緩存處理、日志處理、消息通知等等,這些都可以看作是領域概念中的通用子域,那么對這些通用模塊該如何設計呢?在上一篇中,其實有提到這個,就是開放主機服務(Open Host Service),這是一種協議,可以是 REST 風格,除了這些通用模塊,還有一些是其他項目中會用到本項目中的一些服務,比如一些本項目數據在其他項目中要進行展示,這個我們可以看下園子里的 Home 項目,它其實並不是一個“真正”的項目,而是一個各種服務“聚集地”,也可以看作是一個產品展覽櫃,里面包含有六七個項目,那如果我們在 Home 中分別這些涉及的項目進行實現,想想工作量會有多大,而且如果 Home 變更了,那這些工作都是白費的,那我們該如何設計會比較好呢?好的方式就是不在 Home 項目中進行實現,而是在涉及的本項目中實現,最好就是把這些抽離出來,分別在涉及項目中實現,比如一些數據獲取操作,本項目和 Home 項目都會用到,然后把這些操作用服務的方式發布出來,這樣 Home 就是一個各種服務調用者,涉及項目發布出來的服務也不僅僅只是針對於 Home,也可以用於其他項目,比如消息服務,可以用於各種項目的評論內容回復通知,這就是業務抽離的真正好處:以不變應萬變,因為消息發布是不會變,但其他的業務系統是千變萬化。
我再總結一下上面說的內容,當你開發一個項目的時候,一定要從一個大局觀去看待這個項目,而不只是僅僅局限於本項目中,要了解這個項目所真正蘊含的業務,然后接下來的工作,就是盡可能的去抽離這些業務,這個工作難度可能很大,並且時間成本也很高,但是,當你開發越來越多項目的時候,你就會發現當時的設計是多么的有價值,從一個項目到十個項目,別人會感覺到越來越累,越來越辛苦,但對於你的感覺來說,是越來越輕松,因為原有業務的真正抽離,使得這些項目就像一個個汽車零件,當你研發一款新汽車的時候,由於有很多的汽車零件早已經完成,你所要做的工作,就是把這些汽車零件組裝起來,然后塗裝你喜歡的漂亮顏色(可以看作是 UI),一款嶄新的新汽車這樣輕易完成了。
上面只是一些想法,真正落實起來的難度非常大,也不僅僅是對個人的要求,而是要對整個團隊的要求,有點“站着說話不腰疼”的意思。
2. 經典分層架構
不扯了,言歸正傳,先回顧一下經典分層中的概念:
- 表現層(Presentation Layer):接受用戶輸入和數據展示。
- 應用層(Application Layer):很薄的一層,只包含工作流控制邏輯,不包含業務邏輯。
- 領域層(Domain Layer):核心層,包含整個業務系統的業務邏輯。
- 基礎設施層(Infrastructure Layer):提供整個業務系統的基礎服務。
上面的概念,懂得領域驅動設計的都應該知道,這些是表面上的,那層的具體內部以及各層之間的聯系該如何設計呢?這些內容很雜,而且也不好進行說明,因為沒有統一的做法。除去層的概念,還有一些模塊的概念需要理解,比如 Entity、Value Object、Domain Service、Repository、UnitOfWork、DTO 等等,在層中去運用這些模塊也是一門學問,用的好,你的業務系統就很健壯,用的不好,你的業務系統就是一團亂麻,在經典分層架構設計之前,有兩個基本概念需要牢記在心(依賴倒置原則-DIP):
- 高層模塊不應該依賴於底層模塊,兩者都應該依賴於抽象。
- 抽象不應該依賴於細節,細節應該依賴於抽象。
還需要說明一點,在上面解決方案圖中,你會發現有很多的 XXXX.Tests 項目,這是 XXXX 項目對應的單元測試項目,DDD 和 TDD 並不沖突,反而在 DDD 中,使用 TDD 有相輔相成的作用,關於這一點,就不再探討,在這篇博文中有說明:一個簡單業務用例的回顧和理解。
2.1 領域層(Domain Layer)
先說領域層,因為它是所有層中最重要的,也是核心層。
上面是短消息的領域層項目結構,你可以看作是最簡單、最不完善的領域層。麻雀雖小,但五臟俱全,其中包含 Entity、Value Object、Domain Service、IRepository 等等,也就是說關於領域模型的設計都在領域層中,這是對於架構設計上來說的,對於整個的業務系統來說,領域模型本身和包含的模塊是整個業務系統的核心,所有的業務邏輯都體現在領域模型中,所以,開發人員和領域專家會把更多的時間,去探討領域模型該如何進行設計?
在短消息項目中,領域層就一個項目,但對於復雜性的業務系統來說,一個項目是遠遠不夠的,比如 IDDD 中所說的 ProjectOvation 項目,整個領域就划分為敏捷項目管理核心域、協作子域和身份與訪問通用子域,對於單個的核心域和通用子域來說,又可以划分成多個限界上下文,當然你也可以更加深入的細分這些模塊,這些模塊單個拿出來就比現在的消息領域層復雜的多,所以領域層不只是表面上那么簡單,越多的子域和限界上下文,領域層實現起來就越復雜。
領域層、核心域、子域、限界上下文、類庫項目、領域模型,這些概念並不是一一對應,關於他們之間的關系,我簡單說一下自己的理解,領域層可以看作是很大,它對應的概念是整個領域(Domain),核心域和子域只不過是它的一部分,而限界上下文存在於核心域和子域,關於類庫項目和領域模型,這個沒辦法判斷,但一般來說,一個領域模型只會存在於一個類庫項目中。
領域層的設計沒辦法進行概括,我說一下上面圖中的一個設計不好的地方,在 DomainService、Repositories 文件夾中,其中的接口定義,應該放在獨立的項目中,對於 Domain Service 來說,接口定義和實現都是在領域層中,可能沒關系,但對於 Repository 來說,因為接口定義在領域層,實現在基礎設施層,如果不使用依賴倒置,就會違背 DIP 原則的第一點,而且也會造成循環引用情況的發生。
根據上面第一張圖中,我們可以得知,應用層依賴於領域層和基礎設施層,領域層依賴於基礎設施層,DIP 原則第一點:高層模塊不應該依賴於底層模塊,兩者都應該依賴於抽象。也就是說層與層之間的關系應該依賴於抽象,如果把 Domain Service 和 Repository 的接口獨立出來,這樣應用層和基礎設施層就只需要引用這些接口即可,反過來基礎設施層的接口也一樣。項目中所有的接口對象映射注入獲取,都通過 IoC 進行管理,這是一個獨立的項目,基本上會引用其他所有的項目,就是解決方案中的 Bootstrapper 項目。
2.2 基礎設施層(Infrastructure Layer)
關於基礎設施層,其實也沒什么東西要說明,它和我們使用三層架構中的幫助類類似,其作用都是為這個項目提供最基礎的服務,像一般的日志紀錄、緩存處理、消息通知等等,都會放在基礎設施層,它是唯一貫徹整個項目的一個層,表現層、應用層、領域層都要引用它,在最開始的那張圖中就可以看出來。
除去一些基礎服務,最具話題性的就是 Repository 實現,我記得之前寫過不少博文去探討它,找到相關的兩篇:
你也可以看下最近的這個博問:
因為 Repository 的接口定義在領域層,所以有時候我們會把它和領域層掛鈎,其實並沒有什么關系,Repository 的含義就是倉儲,領域模型對象的存取點,它只管存儲,不管任何的業務邏輯,這個要首先明確,不要把之前的一些業務邏輯封裝成一大串的 Where SQL 代碼,這不是領域驅動設計所干的事。有人會說,為啥要把 Repository 的接口定義放在領域層?其實很簡單,領域層要實現業務邏輯,必然要涉及到領域模型的對象存取(一般是實體對象),比如,我們在領域服務中定義一種業務行為,要對某個實體進行獲取操作,這個我們一般會在上面創建這個實體涉及的 Repository 接口對象,創建方式通過構成函數注入,或者是用 Bootstrapper 進行管理,關於 Repository 的具體實現,領域層絲毫不關系,所以,在業務系統開發的最初階段,開發人員和領域專家可以先進行領域層的設計,即使沒有其他層的實現,領域層的設計也是可以照常進行的,我們一般采取的方式是,對 Repository 的實現用模擬對象方式,比如在 Repository 中定義一個集合的內存對象,然后對它進行一個存儲操作,當領域層設計完成的時候,可以隨時把 Repository 的實現替換掉,比如改成持久化的方式,對於這些操作,絲毫不會影響領域層的設計,因為它依賴的是 Repository 接口,而不是具體實現。
Repository 實現層只和兩個層有關,一個是領域層,另一個就是應用層。對於 Repository 來說,領域層是它的上級,因為接口定義在它那邊,應用層是它的客戶,因為在它那邊被使用。關於 Repository 的使用,又回設計到另一個東西,那就是工作單元(Unit Of Work),之前也寫過關於它的一篇博文:
首先,UOW 和 EF 中的 Context 很類似,其實 Repository 中關於 UOW 接口定義的實現,就是 EF 中的 Context 操作,說白了就是偷懶省事。我再描述一下它的使用,有一個簡單場景:應用層中的一個服務方法,要對多個 Repository 進行操作,而且要進行對象持久化,那具體該如何操作呢?我在上面那個博問中,貼了這樣一段偽代碼:
using (IRepositoryContext repositoryContext = new EntityFrameworkRepositoryContext()) { IContactRepository contactRepository = new ContactRepository(repositoryContext); IMessageRepository messageRepository = new MessageRepository(repositoryContext); .......... repositoryContext.Commit(); }
IRepositoryContext 接口繼承於 IUnitOfWork 接口,在 EntityFrameworkRepositoryContext 的具體實現中,對 UOW 進行了簡單重寫實現,用的就是 EF,所以,你可以把 repositoryContext 對象看作是 UOW,下面是 Repository 對象的創建,傳遞的是 UOW 具體實現,因為在一個 using 塊中,所以,UOW 的生命周期可以跨 Repository 共享,那關於 Repository 中的 UOW 如何定義的呢?其實就是單例實現,也可以進行構造函數注入后進行單例,repositoryContext 訪問的 Commit 操作,其實就是 IUnitOfWork 接口中進行定義的。關於 Repository 的內部實現,在上面 UOW 那篇博文中的一張圖中有詳細說明,就不多說了。
2.3 應用層(Application Layer)
關於應用層的設計,其實,給我印象最深的是這一篇博文:
如果你的領域層設計的不好,最直接的反應就是在應用層中,所以,檢驗你領域驅動設計的好壞,不需要看你的領域層怎么設計的?只需要看應用層的實現代碼就行了,為什么?因為領域層的直接客戶就是應用層,應用層和三層架構中的 BLL 並不一樣,BLL 是業務邏輯層,而應用層只是管理工作流程的進行,它和業務邏輯不掛邊,因為它在業務系統中的職責較小,所以,應用層很薄,在上面那篇博文中,貼出了一段發送短消息的應用層代碼,一看那么長,就知道肯定有問題,這個就不分析了,在那篇博文中有詳細的探討。
我們來看一段標准的應用層代碼:
namespace SaaSOvation.AgilePM.Application.Sprints
{
public class SprintApplicationService { public SprintApplicationService(ISprintRepository sprintRepository, IBacklogItemRepository backlogItemRepository) { this.sprintRepository = sprintRepository; this.backlogItemRepository = backlogItemRepository; } readonly ISprintRepository sprintRepository; readonly IBacklogItemRepository backlogItemRepository; public void CommitBacklogItemToSprint(CommitBacklogItemToSprintCommand command) { var tenantId = new TenantId(command.TenantId); var sprint = this.sprintRepository.Get(tenantId, new SprintId(command.SprintId)); var backlogItem = this.backlogItemRepository.Get(tenantId, new BacklogItemId(command.BacklogItemId)); sprint.Commit(backlogItem); this.sprintRepository.Save(sprint); } } }
上面的代碼摘自 SprintApplicationService.cs,這段代碼的含義就是提交待定項到沖刺,這個業務用例的工作流程很好的在 CommitBacklogItemToSprint 方法中進行了體現,首先,通過 backlogItemRepository 和 sprintRepository 分別獲取待定項對象和沖刺對象,Repository 的創建方式就是通過構造函數獲取,下面最關鍵的一段代碼是 sprint.Commit(backlogItem);
,這是領域層中的內容,應用層不管其如何實現,它只管調用提交待定項到沖刺這個操作,也就是純粹的流程控制,然后再對操作完成的對象進行持久化,就這么簡單,如果在個操作中有很多冗余的操作,和我一樣,那就是失敗的!
2.4 表現層(Presentation Layer)
關於表現層,其實沒有什么好說的,就是應用程序展現的一個東西,可以是 Web 應用程序,也可以是桌面應用程序、也可以是一個服務等等。它是與用戶打交道的窗口,也接受用戶反應的信息,在這其過程中,就必然設計到數據的傳遞,那如何傳遞呢?使用 MVC 中的 View Model?在一般的 Web 應用程序中,可以使用 View Model,但對於領域驅動設計來說,最好的方式是使用 DTO,關於具體的相關信息,可以查看這個博文分類列表(共八篇):
我再補充一下 DTO 的使用,在一開始的解決方案圖中,我們可以看到,DTO 項目的位置,是處在應用層中,而且被獨立出來,其實,我一開始設計是沒獨立的,和應用服務方法放在同一個項目中,但是后來我遇到了一個問題:在應用層中,Repository 獲取的是領域模型對象(實體對象),如果是集合形式的,而且這個領域模型對象非常的龐大,而應用服務方法里面只需要領域模型對象的一部分屬性,這就會造成一些不必要的性能開銷,因為 DTO 是按照表現層和應用層設計的,所以它有一定的針對性,能不能按照 DTO 的設計,進行領域模型對象的獲取呢?其實,實現很簡單,就是使用 AutoMapper 的 Project.To() 操作,按需來獲取屬性對象,但這個實現是在 Repository 內部的,而應用層當時引用的是 Repository 層,如果 Repository 層再進行引用 應用層,就會造成循環引用,最后的改變就是把 DTO 獨立出來,然后供應用層、Repository 層和表現層調用。
上面解決方式看似沒有什么問題,但這種為了解決性能問題,而造成 Repository 的一些破壞,其實是有悖架構設計的,因為 Repository 的含義就是領域模型對象的存儲,它其實是和 DTO 沒半毛錢關系,另外,還有一個嚴重問題是,因為 Repository 的接口定義在領域層,而有些方法簽名返回的是領域模型對象,但實現返回的卻是 DTO 類型對象,這就造成了對領域層的破壞,一個看似小小的 DTO 問題,如果不進行好的設計和處理,就會像“一粒老鼠屎,壞了一鍋粥”這樣嚴重。
上面只是一個問題實例,經過實踐后,你會發現,經典分層架構並不是萬能的,它也存在一些缺陷,其實,使用 CQRS(命令和查詢職責分離)架構,就可以很好的解決上面的問題,領域驅動設計並不只有經典分層架構,你需要打開視野,接受新鮮事物,未完待續~~~
經受我如唐僧一般的啰嗦和折磨,如果你還能堅持看到這,我打算再送你一曲《Only You》:
- only you can take me 取西經
- only you 能殺妖精鬼怪
- only you 能保護我
- .......