在上一篇文章《.NET應用框架架構設計實(shí)踐 - 概述》的評論部分,有網(wǎng)友提出了一個(gè)在面向領(lǐng)域驅動(dòng)架構的實(shí)踐中比較常見(jiàn)的問(wèn)題:“DDD使用聚合根訪(fǎng)問(wèn),那例如那些通用查詢(xún)如何實(shí)現?難道都要經(jīng)過(guò)聚合根多步得到么?DDD如何實(shí)現關(guān)聯(lián)表的查詢(xún),例如3表關(guān)聯(lián)查詢(xún)?”這個(gè)問(wèn)題比較泛,涉及的內容也比較多,我就單獨一篇文章介紹一下我對這個(gè)問(wèn)題的看法。關(guān)于上面問(wèn)題中的“通用查詢(xún)”- 呃,這個(gè)定義比較模糊,我只能給出我的一些想法或者經(jīng)驗性的東西,我在本文中的經(jīng)驗與觀(guān)點(diǎn)并不一定會(huì )100%適合您的應用場(chǎng)景,但我想應該還是具有一定指導性意義的。
我想,還是從聚合根談起吧。聚合根是DDD中的概念,不管是經(jīng)典的DDD架構,還是基于事件驅動(dòng)的CQRS架構,其實(shí)它們之間絕大部分概念都是相通的,比如實(shí)體、值對象、服務(wù)、工廠(chǎng)、倉儲以及聚合/聚合根等。根據我的理解,聚合根是一個(gè)實(shí)體,它保持著(zhù)與其它實(shí)體/值對象的引用,并與這些實(shí)體/值對象一起,來(lái)表達領(lǐng)域的通用語(yǔ)言中的一個(gè)唯一的無(wú)二義的邏輯概念。比如最常見(jiàn)的“客戶(hù)(Customer)”,在“在線(xiàn)銷(xiāo)售”的領(lǐng)域中,“客戶(hù)”不僅包含它所指代的那個(gè)個(gè)人(或者是組織)的名稱(chēng)、聯(lián)系電話(huà)、聯(lián)系電郵,還會(huì )包含它的聯(lián)系地址(Contact Address)以及送貨地址(Delivery Address),那么就Address而言,在此我們可以將其視為值對象,因為我們只關(guān)心地址本身所包含的信息。在這里,“客戶(hù)(Customer)”不僅是實(shí)體,而且是“客戶(hù)-地址”所組成的對象集合(聚合)的聚合根。
在這里會(huì )有異議的地方就是“銷(xiāo)售訂單(Sales Order)”是否應該屬于“客戶(hù)(Customer)”聚合。我覺(jué)得這還是要看在當前的領(lǐng)域中,“銷(xiāo)售訂單”是不是“客戶(hù)”的必有信息,換句話(huà)說(shuō),“客戶(hù)”是不是沒(méi)有“銷(xiāo)售訂單”就不成其為“客戶(hù)”。我想,在大多數情況下,“客戶(hù)”應該是一個(gè)可以脫離“銷(xiāo)售訂單”而單獨存在的實(shí)體,那這樣的話(huà),“銷(xiāo)售訂單”也將不屬于“客戶(hù)”聚合。
現在讓我們來(lái)看“在線(xiàn)銷(xiāo)售”領(lǐng)域中的另一部分:銷(xiāo)售訂單。當然,“銷(xiāo)售訂單(Sales Order)”是實(shí)體,本身也是訂單主體與“訂單明細(Sales Lines)”所組成的聚合的聚合根,這是很自然的事情,因為“銷(xiāo)售訂單”如果沒(méi)有訂單的明細信息,也就失去了訂單本身的意義。此外,“客戶(hù)”實(shí)體也是這個(gè)聚合的一個(gè)組成部分,這也很好理解,“銷(xiāo)售訂單”本身就是客戶(hù)下達的,它不可能脫離“客戶(hù)”而憑空存在。于是,以“銷(xiāo)售訂單”為根的聚合,還包括“客戶(hù)”實(shí)體,以及“訂單明細”(至于“訂單明細”是實(shí)體還是值對象,這跟具體的領(lǐng)域定義有密切關(guān)系,比如如果涉及商品Item與購買(mǎi)量的打折等內容,那么“訂單明細”就需要以實(shí)體方式處理,否則可以設計成“值對象”以減小系統開(kāi)銷(xiāo),本文繞過(guò)這個(gè)問(wèn)題的討論)。在作進(jìn)一步討論之前,讓我們回顧一下DDD中的倉儲。DDD告訴我們,倉儲是作用在聚合根上的:領(lǐng)域模型中對象的保存與讀取都是以聚合為單位而進(jìn)行的。
通過(guò)上面的討論,針對“在線(xiàn)銷(xiāo)售”領(lǐng)域,我們大致得到了如下的領(lǐng)域模型(為了縮短篇幅,圖中可能會(huì )省略某些部分)
問(wèn)題來(lái)了,如果我們需要獲得某個(gè)“客戶(hù)”的所有訂單,該怎么辦?在上面的領(lǐng)域模型中,Customer實(shí)體并沒(méi)有某個(gè)屬性或者方法來(lái)獲得其所有的銷(xiāo)售訂單。那么在遇到這樣的問(wèn)題時(shí),通常都是通過(guò)SalesOrder的倉儲,配合規約(Specification)來(lái)篩選出所有符合特定“客戶(hù)”條件的銷(xiāo)售訂單,然后由倉儲返回銷(xiāo)售訂單的列表。你或許會(huì )覺(jué)得這種做法比較不科學(xué),你會(huì )覺(jué)得應該通過(guò)Customer實(shí)體的某個(gè)屬性(比如SalesOrders)來(lái)獲得該“客戶(hù)”所擁有的所有銷(xiāo)售訂單,這樣會(huì )更直截了當些。但在上面我們已經(jīng)對這個(gè)領(lǐng)域模型進(jìn)行了討論,在我們的案例中,Customer是一個(gè)獨立的實(shí)體,SalesOrder不是它的必要組成部分。于是,為了維護領(lǐng)域模型的完整性,我們需要利用“銷(xiāo)售訂單”的倉儲來(lái)完成這個(gè)功能。偽代碼如下:在上面的代碼中,daxnetOrders對象所保存的就是所有屬于custDaxnet這個(gè)Customer的銷(xiāo)售訂單。通過(guò)這個(gè)例子我們可以看出,當我們需要某些信息的時(shí)候,我們只與領(lǐng)域模型中的聚合、實(shí)體、值對象以及倉儲打交道,我們完全沒(méi)有涉及任何數據庫、數據表、字段、記錄等等這些概念,從上面的代碼也可以看出,我們可以使用服務(wù)樁(Service Stub,PoEAA)模式來(lái)Mock一個(gè)基于內存的倉儲,與關(guān)系型數據庫毫不相干。事實(shí)上也是如此,我們軟件設計者、開(kāi)發(fā)者以及領(lǐng)域專(zhuān)家在同一個(gè)事物上達成共識:領(lǐng)域模型。聚合、實(shí)體、值對象等成為領(lǐng)域模型的主要組成部分,而這些對象又各自保持著(zhù)自己的狀態(tài),也就是我們所需要的數據。在經(jīng)典的DDD架構風(fēng)格(例如Microsoft NLayerApp這樣的架構)中,我們通過(guò)領(lǐng)域模型中的對象及其之間的關(guān)系來(lái)獲得我們所需要的信息,因此,數據的查詢(xún)應該是由倉儲引起,并通過(guò)聚合實(shí)現導航(Navigation)查詢(xún)。接下來(lái),讓我們引入關(guān)系型數據庫,來(lái)談?wù)劚疚淖铋_(kāi)始提出的“多個(gè)表關(guān)聯(lián)查詢(xún)”的問(wèn)題。
在我之前所寫(xiě)的《經(jīng)典的應用系統結構、CQRS與事件溯源》一文中,討論了領(lǐng)域模型與關(guān)系型數據模型之間的“阻抗失衡”效應,在此也就不再重復了,但我們必須弄清楚一件事情,就是在DDD的實(shí)踐中,我們必須拋開(kāi)關(guān)系型數據庫,甚至是其它的一切數據持久化機制,而只關(guān)注領(lǐng)域模型。于是,領(lǐng)域模型本身也需要屏蔽數據持久化的細節內容(我們通常稱(chēng)之為“持久化無(wú)關(guān)性”,Persistence Ignorance)。這有兩個(gè)方面的原因:首先,DDD是面向領(lǐng)域的,不是面向數據的,領(lǐng)域模型對問(wèn)題域進(jìn)行了表述,這也是軟件人員與領(lǐng)域專(zhuān)家的溝通橋梁,如果引入數據存儲的細節內容,既不利于溝通,也會(huì )使得領(lǐng)域模型過(guò)多依賴(lài)具體的技術(shù)實(shí)現方案,提高了系統的耦合度;其次,由于“阻抗失衡”效應的存在,就需要有一個(gè)中介角色來(lái)解決這個(gè)失衡效應,通常是ORM承擔了這個(gè)角色,然而,從技術(shù)實(shí)現的角度看,針對同一個(gè)領(lǐng)域模型,ORM可以有不同的處理方式,具體采用哪種處理方式,可以通過(guò)ORM框架的配置信息(例如,NHibernate的hbm映射文件)來(lái)決定;在這種情況下,領(lǐng)域模型+ORM決定了關(guān)系型數據庫的結構,于是,對數據表、字段、記錄等關(guān)系型數據庫的討論就沒(méi)多大意義了,因為關(guān)系型數據庫本身的結構也是不確定的?,F在,讓我們來(lái)看個(gè)例子,了解一下ORM處理同一個(gè)領(lǐng)域模型的不同方式。就以上文所提到的“客戶(hù) - 地址”聚合為例,ORM處理這個(gè)聚合至少(但不限于)可以有如下四個(gè)方式:
因此,在DDD實(shí)踐中,我們不會(huì )存在“如何進(jìn)行關(guān)聯(lián)表查詢(xún)”這樣的問(wèn)題,我們關(guān)注的是領(lǐng)域模型,至于關(guān)系型數據庫方面的工作,就交給ORM吧。
當然,理論歸理論,實(shí)際項目與理論上的東西相差太大,我們也需要具體問(wèn)題具體分析。例如,ORM的引入雖然解決了領(lǐng)域模型與關(guān)系型數據模型之間的“阻抗失衡”,但也帶來(lái)了一定程度的性能問(wèn)題,對于某些性能要求很高的系統,采用DDD實(shí)踐可能就不是一個(gè)很好的選擇,當然也可以想辦法找一個(gè)折中的方式來(lái)處理問(wèn)題。比如,假設某個(gè)系統基本上對性能要求不高,可以采用DDD的實(shí)踐方式,只是個(gè)別查詢(xún)功能(比如總賬報表生成、數據統計等)要求高效,此時(shí),我們還是可以應用DDD的實(shí)踐經(jīng)驗,并試圖在這幾個(gè)功能上繞過(guò)領(lǐng)域模型,直接采用高效率的數據庫查詢(xún)方式(比如ADO.NET),當然這已經(jīng)脫離了DDD的討論范圍,不過(guò)我們的目的就是為了實(shí)現一套穩定、安全、高效的系統,DDD或不DDD這并不是重點(diǎn),重點(diǎn)在于合適就好。我想,這也是架構師的職責所在吧。
在我們采用“非正常手段”慢慢地繞過(guò)領(lǐng)域模型的時(shí)候,我們會(huì )發(fā)現一個(gè)有趣的現象:其實(shí)“查詢(xún)”根本就不是領(lǐng)域模型的一部分,“查詢(xún)”是可以作為一個(gè)單獨的系統而獨立存在的,在需要的時(shí)候,這個(gè)“查詢(xún)系統”可以被整合到實(shí)際系統當中(比如采用Microsoft Biztalk Server等手段),為客戶(hù)端提供查詢(xún)服務(wù)。既然“查詢(xún)”可以是一個(gè)單獨的系統,那么如何實(shí)現這個(gè)“查詢(xún)”系統,方法也就五花八門(mén)了:可以繼續結合ORM實(shí)現查詢(xún),也可以直接寫(xiě)SQL語(yǔ)句進(jìn)行查詢(xún),甚至還可以使用一些現有的查詢(xún)框架,總之只要能夠向客戶(hù)端提供所需要的數據就行了?!安樵?xún)”不再受到領(lǐng)域模型的牽制,在如此廣泛的技術(shù)選型背景下,我想,要實(shí)現一套復雜的、可定制的查詢(xún)機制根本就不會(huì )是什么難事。
面向領(lǐng)域驅動(dòng)的CQRS(Command Query Responsibility Segregation,命令查詢(xún)職責分離)架構就是這樣一種架構風(fēng)格:它完全將“查詢(xún)”部分從領(lǐng)域模型中分離出來(lái)。
在我之前所寫(xiě)的《EntityFramework之領(lǐng)域驅動(dòng)設計實(shí)踐【擴展閱讀】:CQRS體系結構模式》一文(以下簡(jiǎn)稱(chēng)《CQRS》)中,已經(jīng)非常詳細地對CQRS體系結構模式進(jìn)行了介紹和總結,在這里再對這種結構的“查詢(xún)”部分簡(jiǎn)要地說(shuō)幾句。
在CQRS中,我們可以看到,作用在聚合根上的“倉儲”,已經(jīng)退化成“領(lǐng)域倉儲(Domain Repository)”,領(lǐng)域倉儲也是作用在聚合根上的,但它只有兩個(gè)操作:Save以及GetByAggregateRootId。顯而易見(jiàn),Save的功能就是將整個(gè)聚合保存起來(lái),而GetByAggregateRootId則是通過(guò)聚合根的標識來(lái)獲得整個(gè)聚合。于是,像上面我所例舉的“獲取某個(gè)客戶(hù)的所有銷(xiāo)售訂單”這樣的操作,在CQRS的Command部分是無(wú)法完成的:你無(wú)法通過(guò)規約(Specification)來(lái)獲得“包含”某個(gè)客戶(hù)的所有訂單,你只能夠通過(guò)訂單號來(lái)獲取訂單信息?;蛟S(我是說(shuō)或許),在CQRS架構的領(lǐng)域模型中我們根本無(wú)需知道某個(gè)訂單是屬于哪個(gè)客戶(hù)的,OK,直接將“客戶(hù)”實(shí)體從“銷(xiāo)售訂單”聚合中排除出去。關(guān)于這個(gè)問(wèn)題我在領(lǐng)域驅動(dòng)設計的官方論壇里討論過(guò),得到的結論就是:領(lǐng)域模型只應該包含必要的信息,一切與查詢(xún)有關(guān)的內容,都應該設計在“查詢(xún)”部分。
在《CQRS》一文中我已經(jīng)給出了一張結構圖,現在我再細化一下這個(gè)圖以體現其查詢(xún)部分的具體情況:
在上圖中,領(lǐng)域模型在完成操作之后,會(huì )產(chǎn)生領(lǐng)域事件,在聚合被保存到數據庫的同時(shí),領(lǐng)域事件也會(huì )被發(fā)布到事件總線(xiàn)(Event Bus)上。然后,事件派發(fā)處理器(Event Dispatcher,在這里使用的是Microsoft Biztalk Server)會(huì )將事件派發(fā)到各種不同的訂閱機制,比如Dynamics AX系統或者單獨的查詢(xún)數據庫。這樣,查詢(xún)數據庫將會(huì )有較大的設計空間(比如可以根據客戶(hù)端View Model來(lái)設計關(guān)系型數據庫的表結構),Query Reader的設計也會(huì )變得非常簡(jiǎn)單。在這樣的結構下,實(shí)現通用查詢(xún)、復雜查詢(xún)也會(huì )非常簡(jiǎn)單。總之,領(lǐng)域模型可以提供一定的查詢(xún)能力,比如通過(guò)倉儲、規約以及對象關(guān)系導航等方式獲得所需要的數據,但查詢(xún)應該不是領(lǐng)域模型的組成部分,它是可以被分離出去的。對于經(jīng)典的架構風(fēng)格(比如Microsoft NLayerApp這樣的架構風(fēng)格),如果需要獲得復雜的查詢(xún)功能,那就直接繞過(guò)領(lǐng)域模型,單獨出一個(gè)系統直接訪(fǎng)問(wèn)數據庫進(jìn)行查詢(xún),然后把查詢(xún)返回給客戶(hù)端;客戶(hù)端獲得查詢(xún)結果后,再根據修改過(guò)的數據,通過(guò)倉儲獲得領(lǐng)域對象然后更新領(lǐng)域模型;對于CQRS的架構風(fēng)格,我們將獲得更大的查詢(xún)部分的設計空間,查詢(xún)功能的實(shí)現也不再成為問(wèn)題。
希望本文能夠對關(guān)注這方面內容的讀者朋友一定的幫助。
聯(lián)系客服