用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內的業務,我們就用到了聚合和實體的概念。
實體的定義很簡單,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開頭的詞在前面)
原文地址:https://personball.com/ddd/2018/12/07/from-abp-to-ddd-i
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
- 上一篇: 再不学习我们就out了
- 下一篇: 基于.NET Standard的分布式自