天天看點

IDDD 實作領域驅動設計-架構之經典分層

IDDD 實作領域驅動設計-架構之經典分層
IDDD 實作領域驅動設計-架構之經典分層

在第一張圖中,使用者界面層(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,但看一下實作代碼,你會發現你的真正業務一般會隐藏在這裡面。

我再總結一下上面說的内容,當你開發一個項目的時候,一定要從一個大局觀去看待這個項目,而不隻是僅僅局限于本項目中,要了解這個項目所真正蘊含的業務,然後接下來的工作,就是盡可能的去抽離這些業務,這個工作難度可能很大,并且時間成本也很高,但是,當你開發越來越多項目的時候,你就會發現當時的設計是多麼的有價值,從一個項目到十個項目,别人會感覺到越來越累,越來越辛苦,但對于你的感覺來說,是越來越輕松,因為原有業務的真正抽離,使得這些項目就像一個個汽車零件,當你研發一款新汽車的時候,由于有很多的汽車零件早已經完成,你所要做的工作,就是把這些汽車零件組裝起來,然後塗裝你喜歡的漂亮顔色(可以看作是 UI),一款嶄新的新汽車這樣輕易完成了。

上面隻是一些想法,真正落實起來的難度非常大,也不僅僅是對個人的要求,而是要對整個團隊的要求,有點“站着說話不腰疼”的意思。

不扯了,言歸正傳,先回顧一下經典分層中的概念:

表現層(Presentation Layer):接受使用者輸入和資料展示。

應用層(Application Layer):很薄的一層,隻包含工作流控制邏輯,不包含業務邏輯。

領域層(Domain Layer):核心層,包含整個業務系統的業務邏輯。

基礎設施層(Infrastructure Layer):提供整個業務系統的基礎服務。

上面的概念,懂得領域驅動設計的都應該知道,這些是表面上的,那層的具體内部以及各層之間的聯系該如何設計呢?這些内容很雜,而且也不好進行說明,因為沒有統一的做法。除去層的概念,還有一些子產品的概念需要了解,比如 Entity、Value Object、Domain Service、Repository、UnitOfWork、DTO 等等,在層中去運用這些子產品也是一門學問,用的好,你的業務系統就很健壯,用的不好,你的業務系統就是一團亂麻,在經典分層架構設計之前,有兩個基本概念需要牢記在心(依賴倒置原則-DIP):

高層子產品不應該依賴于底層子產品,兩者都應該依賴于抽象。

抽象不應該依賴于細節,細節應該依賴于抽象。

IDDD 實作領域驅動設計-架構之經典分層

先說領域層,因為它是所有層中最重要的,也是核心層。

上面是短消息的領域層項目結構,你可以看作是最簡單、最不完善的領域層。麻雀雖小,但五髒俱全,其中包含 Entity、Value Object、Domain Service、IRepository 等等,也就是說關于領域模型的設計都在領域層中,這是對于架構設計上來說的,對于整個的業務系統來說,領域模型本身和包含的子產品是整個業務系統的核心,所有的業務邏輯都展現在領域模型中,是以,開發人員和領域專家會把更多的時間,去探讨領域模型該如何進行設計?

在短消息項目中,領域層就一個項目,但對于複雜性的業務系統來說,一個項目是遠遠不夠的,比如 IDDD 中所說的 ProjectOvation 項目,整個領域就劃分為靈活項目管理核心域、協作子域和身份與通路通用子域,對于單個的核心域和通用子域來說,又可以劃分成多個限界上下文,當然你也可以更加深入的細分這些子產品,這些子產品單個拿出來就比現在的消息領域層複雜的多,是以領域層不隻是表面上那麼簡單,越多的子域和限界上下文,領域層實作起來就越複雜。

領域層、核心域、子域、限界上下文、類庫項目、領域模型,這些概念并不是一一對應,關于他們之間的關系,我簡單說一下自己的了解,領域層可以看作是很大,它對應的概念是整個領域(Domain),核心域和子域隻不過是它的一部分,而限界上下文存在于核心域和子域,關于類庫項目和領域模型,這個沒辦法判斷,但一般來說,一個領域模型隻會存在于一個類庫項目中。

領域層的設計沒辦法進行概括,我說一下上面圖中的一個設計不好的地方,在 DomainService、Repositories 檔案夾中,其中的接口定義,應該放在獨立的項目中,對于 Domain Service 來說,接口定義和實作都是在領域層中,可能沒關系,但對于 Repository 來說,因為接口定義在領域層,實作在基礎設施層,如果不使用依賴倒置,就會違背 DIP 原則的第一點,而且也會造成循環引用情況的發生。

根據上面第一張圖中,我們可以得知,應用層依賴于領域層和基礎設施層,領域層依賴于基礎設施層,DIP 原則第一點:高層子產品不應該依賴于底層子產品,兩者都應該依賴于抽象。也就是說層與層之間的關系應該依賴于抽象,如果把 Domain Service 和 Repository 的接口獨立出來,這樣應用層和基礎設施層就隻需要引用這些接口即可,反過來基礎設施層的接口也一樣。項目中所有的接口對象映射注入擷取,都通過 IoC 進行管理,這是一個獨立的項目,基本上會引用其他所有的項目,就是解決方案中的 Bootstrapper 項目。

IDDD 實作領域驅動設計-架構之經典分層

關于基礎設施層,其實也沒什麼東西要說明,它和我們使用三層架構中的幫助類類似,其作用都是為這個項目提供最基礎的服務,像一般的日志紀錄、緩存處理、消息通知等等,都會放在基礎設施層,它是唯一貫徹整個項目的一個層,表現層、應用層、領域層都要引用它,在最開始的那張圖中就可以看出來。

除去一些基礎服務,最具話題性的就是 Repository 實作,我記得之前寫過不少博文去探讨它,找到相關的兩篇:

<a href="http://www.cnblogs.com/xishuai/p/domain_model_repository.html">設計窘境:來自 Repository 的一絲線索,Domain Model 再重新設計</a>

<a href="http://www.cnblogs.com/xishuai/p/ddd_repository.html">Repository 倉儲,你的歸宿究竟在哪?(一)-倉儲的概念</a>

你也可以看下最近的這個博問:

<a href="http://q.cnblogs.com/q/70608/">DDD 模式求解惑</a>

因為 Repository 的接口定義在領域層,是以有時候我們會把它和領域層挂鈎,其實并沒有什麼關系,Repository 的含義就是倉儲,領域模型對象的存取點,它隻管存儲,不管任何的業務邏輯,這個要首先明确,不要把之前的一些業務邏輯封裝成一大串的 Where SQL 代碼,這不是領域驅動設計所幹的事。有人會說,為啥要把 Repository 的接口定義放在領域層?其實很簡單,領域層要實作業務邏輯,必然要涉及到領域模型的對象存取(一般是實體對象),比如,我們在領域服務中定義一種業務行為,要對某個實體進行擷取操作,這個我們一般會在上面建立這個實體涉及的 Repository 接口對象,建立方式通過構成函數注入,或者是用 Bootstrapper 進行管理,關于 Repository 的具體實作,領域層絲毫不關系,是以,在業務系統開發的最初階段,開發人員和領域專家可以先進行領域層的設計,即使沒有其他層的實作,領域層的設計也是可以照常進行的,我們一般采取的方式是,對 Repository 的實作用模拟對象方式,比如在 Repository 中定義一個集合的記憶體對象,然後對它進行一個存儲操作,當領域層設計完成的時候,可以随時把 Repository 的實作替換掉,比如改成持久化的方式,對于這些操作,絲毫不會影響領域層的設計,因為它依賴的是 Repository 接口,而不是具體實作。

Repository 實作層隻和兩個層有關,一個是領域層,另一個就是應用層。對于 Repository 來說,領域層是它的上級,因為接口定義在它那邊,應用層是它的客戶,因為在它那邊被使用。關于 Repository 的使用,又回設計到另一個東西,那就是工作單元(Unit Of Work),之前也寫過關于它的一篇博文:

<a href="http://www.cnblogs.com/xishuai/p/3750154.html">掀起你的蓋頭來:Unit Of Work-工作單元</a>

首先,UOW 和 EF 中的 Context 很類似,其實 Repository 中關于 UOW 接口定義的實作,就是 EF 中的 Context 操作,說白了就是偷懶省事。我再描述一下它的使用,有一個簡單場景:應用層中的一個服務方法,要對多個 Repository 進行操作,而且要進行對象持久化,那具體該如何操作呢?我在上面那個博問中,貼了這樣一段僞代碼:

IRepositoryContext 接口繼承于 IUnitOfWork 接口,在 EntityFrameworkRepositoryContext 的具體實作中,對 UOW 進行了簡單重寫實作,用的就是 EF,是以,你可以把 repositoryContext 對象看作是 UOW,下面是 Repository 對象的建立,傳遞的是 UOW 具體實作,因為在一個 using 塊中,是以,UOW 的生命周期可以跨 Repository 共享,那關于 Repository 中的 UOW 如何定義的呢?其實就是單例實作,也可以進行構造函數注入後進行單例,repositoryContext 通路的 Commit 操作,其實就是 IUnitOfWork 接口中進行定義的。關于 Repository 的内部實作,在上面 UOW 那篇博文中的一張圖中有詳細說明,就不多說了。

關于應用層的設計,其實,給我印象最深的是這一篇博文:

<a href="http://www.cnblogs.com/xishuai/p/3934412.html">Repository 倉儲,你的歸宿究竟在哪?(二)-這樣的應用層代碼,你能接受嗎?</a>

如果你的領域層設計的不好,最直接的反應就是在應用層中,是以,檢驗你領域驅動設計的好壞,不需要看你的領域層怎麼設計的?隻需要看應用層的實作代碼就行了,為什麼?因為領域層的直接客戶就是應用層,應用層和三層架構中的 BLL 并不一樣,BLL 是業務邏輯層,而應用層隻是管理工作流程的進行,它和業務邏輯不挂邊,因為它在業務系統中的職責較小,是以,應用層很薄,在上面那篇博文中,貼出了一段發送短消息的應用層代碼,一看那麼長,就知道肯定有問題,這個就不分析了,在那篇博文中有詳細的探讨。

我們來看一段标準的應用層代碼:

關于表現層,其實沒有什麼好說的,就是應用程式展現的一個東西,可以是 Web 應用程式,也可以是桌面應用程式、也可以是一個服務等等。它是與使用者打交道的視窗,也接受使用者反應的資訊,在這其過程中,就必然設計到資料的傳遞,那如何傳遞呢?使用 MVC 中的 View Model?在一般的 Web 應用程式中,可以使用 View Model,但對于領域驅動設計來說,最好的方式是使用 DTO,關于具體的相關資訊,可以檢視這個博文分類清單(共八篇):

<a href="http://www.cnblogs.com/xishuai/category/577114.html">DTO/AutoMapper</a>

我再補充一下 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 能保護我

.......

本文轉自田園裡的蟋蟀部落格園部落格,原文連結:http://www.cnblogs.com/xishuai/p/iddd-classic-layer.html,如需轉載請自行聯系原作者