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