[2018-12-07]用ABP入門DDD
前言
ABP框架一直以來都是用DDD(領域驅動設計)作為宣傳點之一。但是用過ABP的人都知道,ABP並不是一個嚴格遵循DDD的開發框架,又或者說,它並沒有完整實現DDD的所有概念。
但是反過來說,認真學過DDD的人會發現,所謂“完整實現了DDD,嚴格遵循DDD概念”的開發框架其實並不存在。因為DDD本質上是在分析業務,在“落地”的時候與程式碼有關,但是關係並沒有我們所認為的那麼大。
所以,個人覺得,從學習如何正確使用ABP框架,去揣摩框架的部分功能的設計意圖,也是一種很好的DDD入門方案。
先拋幾個常見問題:
- 名稱空間該如何組織?
- AppService應該怎麼寫?
- 實體類應該充血還是貧血?
- 什麼時候需要寫領域服務(DomainService)?
- 領域事件(DomainEvents)應該怎麼用?
框架並不會嚴格規定我們該怎麼寫程式碼,但是DDD給出了指導性的建議。但如果我們不瞭解DDD,那麼所謂建議就無從說起。
所以,我們還是要從介紹DDD開始。
DDD是一種業務分析方法
DDD領域驅動設計是計算機軟體行業為了專案能儘量趨向成功,根據多年經驗總結出來的一套業務分析的方法論。其核心是消化特定業務領域的知識並建立忠實反映它的軟體模型。
正確的實施並非極其困難,錯誤的實施卻很容易。
DDD並不難,只是中文資料相對缺少,部分詞彙初次接觸有可能覺得過於抽象(加上某些詞的翻譯版本不一樣),會有點晦澀的感覺。
想找中文資料學習DDD的,可以去部落格園搜一下領域驅動設計,這裡首推ENode作者湯雪華的部落格。
本文重點在於普及,不會講的特別深入。
要想講清楚ABP開發框架和DDD的關係,還是要從DDD的作用講起。
DDD的分析部分——頂層設計
DDD有一些詞彙:
- 統一語言
- 問題空間,解決方案空間
- 領域,子領域
- 上下文,繫結上下文(Bounded Context 有些翻譯成邊界上下文,簡稱BC),上下文對映
- 聚合,實體,值物件
- 領域服務,領域事件
在分析部分(也有人稱之為戰略設計,其實就是自上而下的進行分析),我們還不用管聚合、實體、值物件、領域服務、領域事件,只要看前面這些比較抽象的詞彙。
統一語言
DDD的第一件事,是定義“統一語言”。
什麼是統一語言?
大概解釋下,統一語言是為了降低溝通成本(口頭、文件、程式碼等)、減少歧義,通過業務專家(又叫領域專家,就是非常熟悉業務的人)核准和明確語義,專案的官方語言(可以認為是一份術語表,由類似架構師的角色在確認需求的過程中提煉出草案,並後續逐步完善——增加新詞彙,明確語義,處理歧義、同義等)。
寫程式碼最頭疼的命名問題,統一語言可以幫你解決。不僅是參考,還是標準,原則上不允許隨便命名,必須和統一語言保持一致。
問題空間和解決方案空間
問題空間和解決方案空間基本就是字面意思。
形象點說,問題空間是我們在白板上畫的一個大圈圈,寫上“電子商務”。然後大圈圈裡再畫上一些線分割開來,一部分是“C端商城”,一部分是“後臺管理系統”,一部分是“供應鏈系統”。(下圖只是簡化的示意圖,不具備參考意義,真實場景需要更細化)
而解決方案空間,可以理解為針對“問題”的“答案”,解決方案空間的劃分最終對應到我們的程式碼實現,但這個粒度依然是很大的,比如我們用一個VS2017裡解決方案sln(通常是一個單獨的程式碼庫)關聯的所有專案去實現“C端商城”,另一個sln涉及的專案去實現“供應鏈系統”。所有sln合起來是這個“問題空間”的“解決方案空間”。當然有時候簡單系統只需要一個sln就夠了。
除了程式碼的大粒度組織,這往往也影響團隊分工,影響人員組織。
子領域就是對問題空間的繼續劃分。劃分的參考標準是統一語言中的某些詞彙是否出現了歧義——部分詞彙出現多重含義往往預示著存在子領域。每個子領域中的統一語言是一致的,無歧義的。
繫結上下文就是對解決方案空間(不是VS2017那種解決方案)的繼續劃分。
所以子領域對應繫結上下文。
而上下文對映,就是搞清楚繫結上下文之間的關係(上下游依賴關係,下游依賴上游——下游上下文受上游上下文變更影響,通常說的防腐層就是為了隔離這種影響)。
所有這些詞彙,其實核心思想非常簡單,四個字——“分而治之”。
但是具體怎麼“分”,卻沒有固定的方案,完全依賴個人對業務領域的理解程度。甚至這個劃分方案是隨著對業務領域理解的加深而持續變化的。體現到“落地”,就是不斷的調整架構或者重構程式碼。
分析部分最擅長處理的兩種場景
一個場景是,業務邏輯確實很多,很難消化、提煉和組織。就是非常複雜,也是DDD的主要目的——應對軟體核心複雜性。
另一個場景是業務邏輯還沒完全清楚,這一般是指初創企業,特別是創新型企業,沒有行業參照,自己摸索的情況下。
兩個場景都依賴“統一語言”的威力。前者可以通過統一語言促進理解,降低溝通成本。後者可以通過統一語言來表現對業務現狀的理解和展望其未來的走向。
分析部分最重要的兩個元素
統一語言和繫結上下文是DDD分析部分最重要的兩個元素。
繫結上下文繼續向下細分,才會涉及每個繫結上下文的架構問題,此時才開始考慮如何“落地”,也就是下面說的策略部分,選擇支撐架構。
關於DDD分析部分,還涉及很多具體的指導方法,請自行參閱文末所列相關書籍。分析部分進行頂層設計,最重要的產出就是繫結上下文(BC)的劃分及BC之間的關係(上下文對映)。
DDD的策略部分——支撐架構
眾所周知,DDD有一定的前期成本,而它的好處是降低了一個系統後續的長期維護代價。
所以,為每個繫結上下文(BC)選擇支撐架構(實現方案)的指導原則是看“軟體的使用期限”。
上面兩句話其實有一點矛盾——看起來好像是用了就丟的一次性軟體系統不值得使用DDD,但是這個系統的BC是用DDD劃分出來的。
其實這裡的DDD,有歧義,指的是DDD的一個推薦支撐架構——領域模型,而我們前面分析得到這個繫結上下文(BC),是DDD分析部分的一個結果。
也只有到了某個BC是核心業務,需要長期維護、迭代演進的時候,我們才會考慮用領域模型(一種特殊的物件模型)來實現這個BC的支撐架構。到這一步,我們才涉及到諸如OOP開發語言,ABP開發框架這些選擇具體技術棧的問題。
特殊的物件模型意思是,物件模型關注物件和物件之間的關係,即使貧血模型依然是物件模型,特殊是指領域模型關注物件的行為,即要求充血模型。
我們先看看除了領域模型,對於支撐架構還有哪些可能選擇。
CRUD也是一種支撐架構
在看DDD相關的書之前,我們往往認為CRUD相當low,事務指令碼相當low,不管什麼都該用領域模型(這裡不叫DDD了,區分下)來實現。
這就有種,拿著錘子,看什麼都像釘子的感覺。
其實所有DDD相關書籍都在勸我們,具體情況具體分析。
如果是短期、一次性專案(這裡所有的討論都是針對某個BC),一般叫“快速應用程式”,工期緊也是一種考慮因素,自然什麼熟用什麼,CRUD也行,只要行得通。
很多時候優先是解決問題。換句話說:
可以只追求 Make It Work,只要專案是一次性的,無需後續維護的。
再如,一個純展示的專案,可以直接套用一個現成的CMS系統,而非投入人力去從頭開發。
只有當通用軟體產品(財務管理,CRM,CMS之類)無法滿足需求,而且也無法簡單通過一個階段的定製投入就能解決問題時,我們才需要採用領域模型去分析業務,進行軟體建模。
這通常也是老闆為什麼需要組建一個自己的技術團隊的原因。
ABP中的DDD構件
所以,任何開發語言,任何一個能實現CRUD的框架,都可能作為DDD指導下劃分出來的某個BC的支撐架構的實現選擇。DDD並沒有貶低非領域模型式的支撐架構,而是平等的對待它們,因為總有合適的場景,只是依賴個人的經驗。
直到這裡,我們才開始涉及ABP框架。
分而治之,從大到小
前面我們講到在統一語言中根據同個詞彙的多重含義的線索我們可能將一個問題空間劃分成多個子域,為每個子域確定繫結上下文(BC)。這可能涉及到多個VS解決方案(sln檔案),我們先假設只有一個VS解決方案。
我們通常通過ABP官網的專案模板來初始化我們自己專案的VS解決方案。
在下載完成,解壓後,我們可以觀察下程式集名稱和預設名稱空間,這裡可以參考ABP系列——QuickStartB:正確理解Abp解決方案的程式碼組織方式、分層和名稱空間。
接下來以Personball.Demo.sln為例
對於解決方案Personball.Demo.sln,我們發現多數類庫程式集的預設名稱空間是Personball.Demo。再下一層,一般就是實體名稱的複數形式命名的資料夾(跨程式集保持一致)。
注意,名稱空間的層次是沒有限制的,而且預設對應了資料夾層次結構。
所以
對於一個解決方案中容納多個BC,我們可以通過名稱空間來體現BC的隔離。
在BC之上,我們描述架構,可能是一系列草圖,主要用於分析邊界、BC之間的關係,做一些頂層設計。當各個BC的邊界劃分明確後,開始分析一個BC內的業務,我們就用到了聚合和實體的概念。
實體的定義很簡單,ABP有實體的泛型基類Entity<T>
,其中主要就是一個屬性:Id。其他的FullAuditedEntity
或者CreationAuditedEntity
都是框架提供的方便審計的基類擴充套件。
所以,實體就是
領域中具有唯一標識的物件。
從名稱空間上看,我們可以給BC一個名字,讓它邏輯上“統領”一部分程式碼,這些程式碼主要就是一些實體類。但是實體類也是有主次之分的。典型的例子就是Order實體和OrderItem實體。雖然OrderItem有自己的id,但我們幾乎不會單獨引用OrderItem,因為單獨一條OrderItem幾乎不會有業務意義(不能說死,不排除個別我沒見識過的業務場景)。一個Order有多個OrderItem,對OrderItem的操作通過Order進行代理,這裡,Order就是聚合根。
把一組實體放一起,就是聚合,其中作為主要代表的實體即是聚合根。聚合之間只能通過聚合根進行引用,不能直接引用聚合中的非聚合根實體。
按Order來說,其他聚合要引用Order的時候,記錄的是OrderId(或者訂單號),假設其他聚合要處理某個Order的OrderItem,它也只能引用Order,讓Order去處理它自己的OrderItem。這其實是一種內聚的思想,或者叫封裝,或者叫關注點分離,總之是一種複雜性的隔離(劃分BC也是一種複雜性的隔離)。
我們一開始看到ABP的AggregateRoot<T>
和IAggregateRoot<T>
,幾乎是懵的,專案模板中也沒有這個基類的範例。再看看這個基類提供的屬性DomainEvents
,以及ABP框架中涉及該屬性機制的原始碼(看AbpDbContext的SaveChange方法實現)。這時候,我們看到了事件怎麼用,開始思考領域事件這個詞,開始去學習DDD。
當我們開始思考事件的時候,我們很自然的就會去思考實體的行為(方法)。
我們通過實體方法實現實體自己能夠處理的業務邏輯。以“Tell,Not Ask”的原則實現實體的行為。在行為成功完成後,丟擲事件,以便外部協同。而聚合根(繼承AggregateRoot<T>
基類或者實現IAggregateRoot<T>
介面)作為其他實體的代理,實現本聚合內的邏輯,通過DomainEvents
收集各類事件,交由ABP框架底層來觸發事件,實現跨聚合甚至跨BC的協同(同時事件的釋出訂閱模式也是一種邏輯程式碼的解耦,順序無關,EventHandler也可以回滾工作單元)。
另外,DDD中的倉儲模式是基於聚合根實體的(聚合根同時代理了非聚合根實體的倉儲職責,就是說OrderItem不應該有自己的倉儲介面和實現),這一點在ABP中並沒有嚴格限制,或許是ABP作者不希望把框架的使用門檻定的太高。
實體(聚合根也是實體),只能實現自己控制範圍內的業務邏輯,控制範圍外的呢?
所有無法放到單個實體內實現的業務邏輯,都可以放到領域服務中實現。
這包含,需要同一個實體類的多個例項配合的,需要不同實體類的多個例項配合的,還有其他。只要一個實體的例項無法自己完成這部分邏輯,就需要構建領域服務。
最後,最小的DDD構件,值物件。ABP框架中有一個基類ValueObject<T>
,即用來表示值物件。
其實DDD中的值物件對應到程式碼,有一個很寬泛的範圍,可以認為
所有沒有唯一標識的資料物件,都是值物件。
最基本的,比如C#語言的值型別,像string,int,decimal,都是值物件。那麼我們為什麼還需要一個基類來輔助構造值物件?
第一個原因是,值型別,業務表達能力弱。
通過float,我們可以知道數量,但是不知道是重量還是體積;
通過decimal我們能表示金額,但是不知道是人民幣還是美元。
所以,我們需要自己構建值物件,來更準確的表達業務概念。
第二個原因是,方便。
值物件只能通過各個屬性的具體值比較來唯一確定,這個基類幫我們重寫了Equals()
和GetHashCode()
,並重載了相等和不等操作符。
但,這裡有個坑
值物件必須保證其不變性
具體看Abp系列——為什麼值物件必須設計成不可變的,而ABP框架是無法控制你如何使用ValueObject<T>
的子類的。具體地說,
你的值物件必須關閉所有屬性的setter,必須通過建構函式來初始化,且不允許通過方法改變屬性值。
忘了分層,應用服務層和基礎設施層
上面講的(聚合、聚合根、實體、值物件、領域服務、領域事件)基本都是領域層。
DDD講領域模型支撐架構的時候,特別提到分層,也是我們從ABP中學到的分層方式:表現層、應用服務層、領域層、基礎設施層。
- 表現層並不特指前端介面,MVC框架也只是一種表現層框架,它只是特別擅長處理Http協議。
- 應用服務層就是Application程式集,是DDD建議的體現用例的一層,直接對接表現層(類似MVC控制器的協調作用,接受請求,返回DTO/ViewModel),用來編排任務,將工作指派給下層。所以應用服務(AppService)的程式碼,根據用例進行組織即可。
- 領域層即是業務模型的完整實現。
- 基礎設施層側重於持久化技術,比如EF,但是不限於持久化技術(通用功能介面的具體技術實現,類似倉儲,介面定義在領域層,實現放在基礎設施層)。ABP按照ORM框架名稱作為基礎設施層的程式集命名可以理解,但不能被其限制。個人建議另開一個程式集如Personball.Demo.Infrastructure,依賴於Personball.Demo.EntityFramework,再讓啟動模組依賴Infrastructure模組。
擴充套件:CQRS和事件溯源
當我們說經典領域模型的時候,指的就是基於物件模型來實現業務,資料儲存走關係型資料庫,一切看起來都很完美。
但是DDD研究的是複雜性。
軟體開發行業幾十年的經驗累積下,前輩們發現如果把軟體功能分成兩方面,假設系統中查詢部分的複雜度是N,命令(建立或變更資料)部分的複雜度也是N。
那麼經典領域模型的情況下,系統的命令和查詢混在一起,這個總體複雜度就是N乘以N,如果分開,那麼系統總體複雜度就會降低到N加N。
另一種說法是,物件模型的侷限性日益顯現,現在發現關注事件比關注物件更方便業務建模,因為現實世界是基於事件的。這引導我們可以使用函數語言程式設計來實現支撐架構,同時也引出了事件溯源架構。
CQRS,命令與查詢職責分離,正如其字面上的意思,一個相當簡單的原則,卻非常有效的降低了系統的複雜性。
這裡並不是要推薦一個CQRS開發框架,只是提一下,大家可以在任何開發框架,任何場景下,按CQRS的方式去思考,都可以獲得實際的好處。
再理一遍
- 統一語言
- 問題空間、子領域
- 解決方案空間、繫結上下文/上下文對映、聚合/聚合根、實體、值物件
如果還有不明白的,可以參考下列書籍;如果還想深入學習的,可以參考下列書籍。
希望本文能對你有所啟示,由於本人水平有限,若有表達錯誤的地方,歡迎斧正。
相關書籍
《Microsoft.Net企業級應用架構設計》
架構師參考書,後半本基本都是講DDD的,也是本文的主要參考(這本最近剛重新看完,也在整理思維導圖,下面幾本專講DDD的還沒複習,忘得差不多了)
《領域驅動設計》
又稱DDD
《實現領域驅動設計》
又稱IDDD
《領域驅動設計模式、原理與實踐》
又稱PPPDDD(英文版書名三個P開頭的詞在前面)