EntityFramework之領域驅動設計實踐 - 前言程序員
EntityFramework之領域驅動設計實踐 (一):從DataTable到EntityObject數據庫
EntityFramework之領域驅動設計實踐 (二):分層架構express
EntityFramework之領域驅動設計實踐 (三):案例:一個簡易的銷售系統編程
EntityFramework之領域驅動設計實踐 (四):存儲過程 - 領域驅動的反模式設計模式
EntityFramework之領域驅動設計實踐 (五):聚合緩存
EntityFramework之領域驅動設計實踐 (六):模型對象的生命週期 - 工廠服務器
EntityFramework之領域驅動設計實踐 (七):模型對象的生命週期 - 倉儲網絡
EntityFramework之領域驅動設計實踐 (八):倉儲的實現(基本篇)session
EntityFramework之領域驅動設計實踐 (九):倉儲的實現(深刻篇)數據結構
EntityFramework之領域驅動設計實踐 (十):規約(Specification)模式
EntityFramework之領域驅動設計實踐【擴展閱讀】:服務(Services)
EntityFramework之領域驅動設計實踐【擴展閱讀】:CQRS體系結構模式
EntityFramework之領域驅動設計實踐 - 前言
根據網友的討論結果,以及本身在實踐中的不斷積累,在整理的過程當中,我會將原文中的描述做相應調整。不只如此,也有很多關心領域驅動設計的網友在原文的評論欄目中提了問題或做了批註,我也針對網友的問題給予了細緻的答覆,爲了可以讓更多的朋友瞭解到問題的本質,本次整理稿會將評論部分也一一列出,供你們參考。
EntityFramework
EntityFramework是微軟繼LINQ to SQL以後推出的一個更爲完整的領域建模和數據持久化框架。初見於.NET Framework 3.5版本,4.0的.NET Framework已經集成了EntityFramework。使用.NET 4.0的朋友就不須要下載和安裝額外的插件了。與LINQ to SQL相比,EntityFramework從概念上將系統設計的關注點從數據庫驅動轉移到模型/領域驅動上。
領域驅動設計(DDD)
領域驅動設計並非一門技術,也不是一種方法論。它是一種考慮問題的方式,是一種經驗積累,它關注於那些處理複雜領域問題的軟件項目。爲了得到項目成功,團隊須要具有一系列的設計實踐、開發技術和開發準則。與此相關的技術與設計/代碼重構也是領域驅動設計討論的重點。
本系列文章就是着重討論EntityFramework在領域驅動設計上的實踐,也但願DDD與.NET的愛好者可以從文中得到啓發,將解決方案用在本身的實際項目中。
從DataTable到EntityObject
雖然從技術角度講,DataTable與EntityObject並無什麼可比性,然而,它暗示了一場革命正在悄然進行着,即便是微軟,也擺脫不了這場革命的颶風。
軟件設計思想須要革命,須要擺脫原有的思路,而走向面向領域的道路。你或許會以爲聽起來很玄乎,然而目前軟件開發的現狀使你不得不接受這樣的現實,仍然有大幫的從業人員整天扯着數據庫不放,仍然有大幫的人在問:「我要實現xxxx功能,個人數據庫應該如何設計?」這些人犯了根本性的錯誤,就是把軟件的目的搞錯了,軟件研究的是什麼?是研究如何使用計算機來解決實際(領域)問題,而不是去研究數據應該如何保存更合理。這方面的事情我在我之前的博文中已經說過不少次了,在此就再也不重複了。
固然,不少讀者會跟我有着相同的觀點,也會以爲我很「火星」,但這都沒關係,上面我所說的都是一個引子,但願可以幫助更多「步入歧途」的從業人員 「走上正路」。不錯,軟件設計應該從「數據庫驅動」走向「領域驅動」,而領域驅動設計的實踐經驗正是爲設計和開發大型複雜的軟件系統提供了實踐指導。
回到咱們的副標題,從DataTable到EntityObject,你看到了什麼?看到的是微軟在領域驅動上的進步,從DataTable這一純粹的數據組織形式,到EntityObject這一實體對象,微軟帶給咱們的不只僅是技術框架,更是一套面向領域的解決方案。
.NET 4.0來了,隨之而來的是實體框架(EntityFramework,簡稱「EF」),在本系列文章中,我將結合領域驅動設計的實踐知識,來剖析EF的具體應用過程,固然,如今的EF還並非那麼完善,我也很是期待可以看到,從此微軟可以繼續發展和完善EF,使其成爲微軟領域驅動工具箱中的重要角色。
先不說EF,首先咱們簡要地分析一下,做爲一種框架,要支持領域驅動的思想,須要知足哪些硬性需求,以後再仔細想一想,.NET EF是否真的可以很好地應用在領域驅動上。
從上面的描述,咱們對EF的功能有了個大概的瞭解,接下來的系列文章,我會和你們一塊兒,一步步地探討,如何在EF上應用領域驅動設計的思想,進而完成咱們的案例程序。本系列文章均爲我我的原創,或許在某些問題上你會有不一樣意見,沒關係,你能夠直接簽寫留言,或者發郵件給我,期待與你的探討,期待與你在軟件架構設計的道路上共同進步。
分層架構
在引入實例之前,咱們有必要回顧,並進一步瞭解分層架構。「層」是一種體系結構模式[POSA1],也是被廣大軟件從業人員用得最爲普遍並且最爲靈活的模式之一。記得在CSDN上,時常有朋友問到:「分層是什麼?爲何要分層?三層架構是否是就是表現層、業務邏輯層和數據訪問層?」
到這裏,你可能會以爲這些朋友的問題很簡單,分層嘛,不就是將具備不一樣職責的組件分離開來,組成一套層內部高聚合,層與層之間低耦合的軟件系統嗎?不錯!這是分層的目標。可是,咱們應該如何分層呢?
領域驅動設計的討論一樣也是創建在層模式的基礎上的,但與傳統的分層架構相比,它更注重領域架構和技術架構的分離。
傳統的三層架構
如上文那位朋友提的問題那樣,最簡單的分層方式天然就是「表現層、業務邏輯層和數據訪問層」,咱們能夠用下圖來表示這個思想:
注意圖中打虛線的「基礎結構層」,從實踐的表現上來看,這部份內容可能就是一些幫助類,好比 SQLHelper之類的,也多是一些工具類,好比TextUtility之類。這些東西能夠被其它各層所訪問。而基於分層的概念,表現層只能跟業務邏輯層打交道,而業務邏輯層在數據持久化方面的操做,則依賴於數據訪問層。表現層對數據訪問層的內容一無所知。
從領域驅動的角度看,這種分層的方式有必定的弊端。首先,爲各個層面提供服務的「基礎結構層」的職責比較紊亂,它能夠是純粹的技術框架,也能夠包含或處理必定的業務邏輯,這樣一來,業務邏輯層與「基礎結構層」之間就會存在依賴關係;其次,這種結構過度地突出了「數據訪問」的地位,把「數據訪問」與 「業務邏輯」放在了等同的地位,這也難怪不少軟件人員一上來就問:「個人數據表該如何設計?」
領域驅動設計的分層
領域驅動設計將軟件系統分爲四層:基礎結構層、領域層、應用層和表現層。與上述的三層相比,數據訪問層已經不在了,它被移到基礎結構層了。
從上圖還能夠看到,表現層與應用層之間是經過數據傳輸對象(DTO)進行交互的,數據傳輸對象是沒有行爲的POCO對象,它的目的只是爲了對領域對象進行數據封裝,實現層與層之間的數據傳遞。爲什麼不能直接將領域對象用於數據傳遞?由於領域對象更注重領域,而DTO更注重數據。不只如此,因爲「富領域模型」的特色,這樣作會直接將領域對象的行爲暴露給表現層。
從下一個章節開始,我將以最簡單的銷售系統爲例,介紹EntityFramework下領域驅動設計的應用。
案例:一個簡易的銷售系統
從如今開始,咱們將以一個簡易的銷售系統爲例,探討EntityFramework在領域驅動設計上的應用。爲了方便討論,咱們的銷售系統很是簡單,不會涉及客戶存在多個收貨地址的狀況,也不會包含任何庫存管理的內容。假設咱們的系統只須要維護產品類型、產品以及客戶信息,並可以幫客戶下訂單、跟蹤訂單狀態,以及接受客戶退貨。從簡單的分析咱們大體能夠了解到,這個系統將會有以下實體:客戶、單據、產品及其類型。單據分爲銷售訂單和退貨單兩種,每一個單據能夠有多個單據行(好比銷售訂單行和退貨單行)。不只如此,系統容許每一個客戶有多張信用卡,以便在結帳的時候,選擇一張信用卡進行支付。在使用EF的Entity Data Model Designer進行設計後,咱們獲得下面的模型:
上面的模型表述了領域模型中各個實體及其之間的關係。咱們先不去討論整個系統的業務會是什麼樣的,咱們先看看EF是如何支持實體和值對象概念的。
實體
首先看看實體這個概念。在領域驅動設計的理論中,實體是模型中須要區分個體的對象,也就是說,針對某種類型,咱們既要知道它是什麼,還須要知道它是哪一個。我在前面的博文中有介紹過實體這個概念。實體都有一個標識符,以便跟同類型的其它實體進行區分。EF Entity Data Model Designer上可以畫出的都是實體,你能夠看到每一個實體都有個Id成員,其Entity Key屬性被設置爲True,同時被分配了一種標識符的生成方式,以下圖所示:
在從領域模型映射到數據模型的過程當中,這個標識符一般都是被映射爲關係數據庫某個數據表的主鍵,這個應該是很容易理解的。
其次,EF不支持實體行爲,所以,整個模型只能被稱爲Entity Data Model,而不是Entity Model,由於它只支持對實體數據的描述。幸好從.NET 2.0開始,託管語言開始支持partial特性,同一個類能夠以部分類(partial class)的特性寫入多個代碼文件中。所以,若是須要向上述模型中的實體加入行爲,咱們能夠在工程中加入幾個代碼文件,而後使用部分類的特色,爲實體添加必要的行爲。好比,下面的部分類向訂單行中加入了一個只讀屬性,該屬性用於計算某一單據行所擁有的總金額:
有朋友會問,爲何咱們要另外使用部分類,而不是直接在模型文件 edmx的源代碼上直接修改?由於這個源代碼文件是框架動態生成的,若是在上面修改,等下次模型被更新的時候,你所作的更改便會丟失。
對於實體的行爲,EF支持從數據庫存儲過程生成實體對象行爲的過程。對此,我持批判態度:EF把數據模型與實體模型混爲一談了,這種作法只能讓軟件人員感到更加困惑。我在下一篇文章將重點表述我對這個問題的見解。我也相信微軟在下一代實體框架中可以處理好這個問題。
再次,EF對實體對象關係的支持主要有關聯和繼承。根據Multiplicity的設置,關聯又能夠支持到組合關聯與聚合關聯。我以爲EF中對繼承關係的支持是一個亮點。繼承表述了「什麼是一種什麼」的觀點,好比在咱們的案例中,「銷售訂單」和「退貨單」都是一種「單據」。若是從傳統的數據庫驅動的設計方案,咱們很天然地會使用「Orders」數據表中的整型字段「OrderType」來保存當前單據的類型(好比0表示銷售訂單,1表示退貨單),那麼,在獲取系統中全部銷售訂單的時候,咱們會使用下面的代碼:
List<Order> GetSalesOrders(IDbConnection connection) { IDbCommand command = new SqlCommand("SELECT * FROM [Orders] WHERE [OrderType]=0", (SqlConnection)connection); List<Order> orders = new List<Order>(); using (IDataReader dr = command.ExecuteReader()) { while (dr.Read()) { Order order = new Order(); order.Id = Convert.ToInt32(dr["Id"]); // ... orders.Add(order); } dr.Close(); } return orders; } |
從技術角度講,上面的代碼沒什麼問題,運行的也很好,可以得到系統中全部銷售訂單的列表。可是, [OrderType]=0這種寫法並不包含任何領域語義,若是讓另外一個開發人員來跟進這段代碼,他不得不先去查閱其它項目文檔,以瞭解這個 [OrderType]=0的具體涵義。在引入了繼承關係的EF中,咱們只須要下面的Linq to Entities,便可既方便、又明瞭地得到全部銷售訂單的列表了:
List<Order> GetSalesOrders() { using (EntitiesContainer container = new EntitiesContainer()) { return (from order in container.Orders where order is SalesOrder select order).ToList(); } } |
簡單明瞭吧?EF帶給咱們的不只僅是一個技術框架,也不只僅是一個數據存取的解決方案。
值對象
EF支持值對象,這很好!在EF中能夠定義Complex Types,而一個Complex Type下能夠定義多個Primitive Type和多個Complex Type。與LINQ to SQL相比,這是一大進步。
對於值對象的兩點問題我在第一篇文章中已經講過了,在此就不重複了。
綜上所述,EF基本上可以支持領域驅動設計的思想(即便有些方面不完善,但目前也能夠找到替代的方案)。我想,只要可以對領域驅動有清晰的認識,就可以很好地將實體框架應用於領域驅動的實踐中。
存儲過程 - 領域驅動的反模式
EntityFramework(EF)中有一項功能,就是可以根據數據庫中的存儲過程生成實體的行爲(或稱方法,如下統稱方法)。我在本系列的第一篇博文中就已經提到,這種作法並不可取!由於存儲過程是技術架構中的內容,而咱們所關注的倒是領域模型。
Andrey Yemelyanov在其「Using ADO.NET EF in DDD: A Pattern Approach」一文中,有下面這段話:
In this context, the architect also identified the anti-pattern of using the EF (ineffective use): the EF is used to mechanically derive the domain model from the database schema (database-driven approach). Therefore, the main topic of the guidelines should be the domain-driven development with the EF and the primary focus should be on how to build a conceptual model over a relational model and expose it to application programmers.
黑體部分的意思是:(被採訪的架構師)同時也指出了使用EF的一種「反模式」,即經過使用數據庫結構來機械化地生成領域模型(數據庫驅動的方式)。這也就證實了我在第一篇文章中指出的「只能經過領域模型來映射到數據模型,反過來則不行」的觀點。
我可以理解不少作系統的朋友喜歡考慮數據庫方面的事情,畢竟數據存儲也是軟件系統中不可或缺的部分(固然,內存數據庫另當別論),數據庫結構設計的好壞直接影響到系統的運行效率。好比:加不加索引、如何加索引,將對查詢效率形成重大影響。但請注意:你把過多的精力放在了技術架構上,而本來更重要的業務架構卻被扔到了一邊。
何時選擇存儲過程?我認爲存儲過程的操做對象應該是數據,而不是對象,業務層也不可能把業務對象交給數據庫去處理。其實,存儲過程自己的意義也就是將數據放在DB服務器上處理,以減小客戶程序與服務器之間的網絡流量和往返次數,從而提升效率。因此,能夠把查詢次數較多的、與業務無關的數據處理交給存儲過程(好比數據統計),而不要單純地認爲存儲過程可以幫你解決業務邏輯問題,那樣作只會讓你的領域模型變得混亂,長此以往,你將沒法應對複雜的業務邏輯與需求變動。
在作技術選型的時候還要注意一點,也就是存儲過程的數據庫相關性。存儲過程是特定於某種關係型數據庫機制的,好比,SQL Server的存儲過程與Oracle的存儲過程並不通用,而有些數據庫系統甚至不支持存儲過程。所以過多使用存儲過程將會給你帶來一些沒必要要的麻煩。我我的對是否使用存儲過程給出以下幾點意見:一、根據需求來肯定;二、不推薦使用;三、出於效率等技術考慮,須要使用的,酌情處理。
回過頭來看實體框架,雖然如今支持從數據庫存儲過程生成實體方法的過程,但這是一種反模式,我也不但願從此EF還提供將實體方法映射到存儲過程的功能,由於行爲不一樣於數據,數據是狀態,而行爲是業務,它與領域密切相關,它不該該被放到「基礎結構層」這一技術架構中。
聚合
聚合(Aggregate)是領域驅動設計中很是重要的一個概念。簡單地說,聚合是這樣一組領域對象(包括實體和值對象),這組領域對象聯合起來表述一個完整的領域概念。好比,根據Eric Evans《領域驅動設計》一書中的例子,一輛車包含四個輪子,輪子離開「車」就毫無心義,此時這個聯合體就是聚合,而「車」就是聚合根(Aggregate Root)。
從實踐中得知,並不是領域模型中的每一個實體都可以完整地表述一個明確的領域概念,就好比客戶與送貨地址的關係。假設在某個應用中,系統須要爲每一個客戶維護多個送貨地址,此時送貨地址就是一個實體,而不是值對象。那麼這樣一來,領域模型中至少就有了「客戶」和「送貨地址」兩個實體,而事實上,「送貨地址」是針對「客戶」的,離開「客戶」,「送貨地址」就變得毫無心義。因而,「送貨地址」就和「客戶」一塊兒,完整地表達了「客戶能夠有多個送貨地址,並能對它們進行維護」的思想。
在《實體框架之領域驅動實踐(三) - 案例:一個簡易的銷售系統》一文中,咱們簡單地設計了一個領域模型,其中包含了一些必要的實體和值對象。如今,我用不一樣顏色的筆在這個領域模型上圈出了三個聚合:客戶、訂單以及產品分類,以下圖所示:
【注意】:若是像上圖所示,Category-Item組成一個聚合,那麼此時聚合根就應該是Item,而不是Category,由於Category對Item從概念上並無包含/被包含的關係,而更多狀況下,Category是 Item的一種信息描述,即某個Item是能夠歸類到某個Category的。在這種狀況下,咱們不須要對Category進行維護,Category就以值對象的形式存在於領域模型中。若是是另外一種應用場合,好比,咱們的系統須要針對Category進行促銷,那麼咱們須要維護Category的信息,由此Category和Item就分屬兩個不一樣的聚合,聚合根爲各自自己。
首先是「客戶-信用卡」聚合,這個聚合表示了一個客戶能夠擁有多張信用卡,相似於上面所講的 「客戶-送貨地址」的概念;其次是「訂單-訂單行」的聚合,相似地,雖然訂單行也是一個實體,由於在應用中須要對每一個訂單行進行區分,可是訂單行離開訂單就變得毫無心義,它是「訂單」概念的一部分;最後是「產品分類-產品」的聚合。
每一個聚合都有一個根實體(聚合根,Aggregate Root),這個根實體是聚合所表述的領域概念的主體,外部對象須要訪問聚合內的實體時,只能經過聚合根進行訪問,而不能直接訪問。從技術角度考慮,聚合肯定了實體生命週期的關注範圍,即當某個實體被建立時,同時須要建立以其爲根的整個聚合,而當持久化某個實體時,一樣也須要持久化整個聚合。好比,在從外部持久化機制重建「客戶」對象的同時,也須要將其所擁有的「信用卡」賦給「客戶」實體(具體如何操做,根據需求而定)。不要去關注聚合內實體的生命週期問題,若是你真的這麼作了,那麼你就須要考慮下你的設計是否合理。
由此引出了「領域對象生命週期」的問題,這個問題我會在後面兩節單獨討論,但目前至少知道:
1.領域對象從無到有的建立,不是針對某個實體的,而是針對某個聚合的
2.領域對象的持久化(一般所說的「保存」)、重建(一般所說的「查詢」)和銷燬(一般所說的「刪除」)也不是針對某個實體的,而是針對某個聚合的
很惋惜,微軟的EntityFramework(實體框架,EF)目前並不支持「聚合」的概念,全部的實體都被一股腦地塞到 ObjectContext中:
爲了實現聚合的概念,咱們又一次地須要用到「部分類(partial class)」的功能。咱們首先定義一個IAggregateRoot的接口,修改每一個聚合根的實體類,使其實現IAggregateRoot接口,以下:
public interface IAggregateRoot { } |
[AggregateRoot("Orders")] partial class Order : IAggregateRoot { public Single TotalDiscount { get { return this.Lines.Sum(p => p.Discount); } } public Single TotalAmount { get { return this.Lines.Sum(p => p.LineAmount); } } } |
到這裏又有問題了,接口IAggregateRoot中什麼都沒有定義?!我在個人技術博客中,特別解釋了C#中接口的三種用途,請參考這篇文章:《C#基礎:多功能的接口》。在這裏,咱們將IAggregateRoot接口用做泛型約束。在看完後續的兩篇介紹領域對象生命週期的文章後,你就可以更好地理解這個問題了。事實上,在領域驅動設計的社區中,很多人都是這樣用的。
最後說明一下,因爲實體框架使全部的實體類繼承於EntityObject類,而從面向對象的角度,接口是沒辦法去繼承於類的,所以,在這裏咱們的 IAggregateRoot接口好像跟實體沒什麼太大的關係,而事實上聚合根應該是一種實體。在不少領域驅動的項目中,設計人員專門設計了 IEntity接口,全部實現了該接口的類都被認定爲實體類,因而,IAggregateRoot接口也就很天然地繼承IEntity接口,以表示「聚合根是一種實體」的概念,代碼大體以下:
public interface IEntity { Guid Id { get; set; } } public interface IAggregateRoot : IEntity { } |
總的來講,領域模型須要根據領域概念分紅多個聚合,每一個聚合都有一個實體做爲「聚合根」,通俗地說,領域對象從無到有的建立,以及CRUD操做都應該做用在聚合根上,而不是單獨的某個實體。當你的代碼須要直接對聚合內部的實體進行CRUD操做時,就說明你的模型設計已經存在問題了。
模型對象的生命週期 - 工廠
首先應該認識到,是對象就有生命週期。這一點不管在面嚮對象語言仍是在領域驅動設計中都適用。在領域驅動設計中,模型對象生命週期能夠簡要地用下圖表示:
經過上圖能夠看到,對象經過工廠從無到有建立,建立後處於活動狀態,此時能夠參與領域層的業務處理;對象經過倉儲實現持久化(也就是咱們常說的「保存」)和重建(也就是咱們常說的「讀取」)。內存中的對象經過析構而消亡,處於持久化狀態的對象則經過倉儲進行撤銷(也就是咱們常說的「刪除」)。整個狀態轉換過程很是清晰。
如今引出了管理模型對象生命週期的兩種角色:工廠和倉儲。同時也須要注意的是,工廠和倉儲的操做都是基於聚合根(Aggregate Root)的,而不只僅是針對實體的。關於倉儲,內容會比較多,我在下一節單獨講述。在本節介紹一下工廠在.NET實體框架(EntityFramework)中的實現。
在打開了.NET實體框架自動生成的Entity Data Model Source Code文件後,咱們發現,.NET實體框架爲每個實體添加了一個工廠方法,該方法包含了一系列原始數據類型和值類型的參數。好比,咱們案例中的 Customer實體就有以下的代碼:
/// <summary> /// Create a new Customer object. /// </summary> /// <param name="id">Initial value of the Id property.</param> /// <param name="name">Initial value of the Name property.</param> /// <param name="billingAddress">Initial value of the BillingAddress property.</param> /// <param name="deliveryAddress">Initial value of the DeliveryAddress property.</param> /// <param name="loginName">Initial value of the LoginName property.</param> /// <param name="loginPassword">Initial value of the LoginPassword property.</param> /// <param name="dayOfBirth">Initial value of the DayOfBirth property.</param> public static Customer CreateCustomer(global::System.Int32 id, Name name, Address billingAddress, Address deliveryAddress, global::System.String loginName, global::System.String loginPassword, global::System.DateTime dayOfBirth) { Customer customer = new Customer(); customer.Id = id; customer.Name = StructuralObject.VerifyComplexObjectIsNotNull(name, "Name"); customer.BillingAddress = StructuralObject.VerifyComplexObjectIsNotNull(billingAddress, "BillingAddress"); customer.DeliveryAddress = StructuralObject.VerifyComplexObjectIsNotNull(deliveryAddress, "DeliveryAddress"); customer.LoginName = loginName; customer.LoginPassword = loginPassword; customer.DayOfBirth = dayOfBirth; return customer; } |
那麼在建立一個Customer實體的時候,就能夠使用 Customer.CreateCustomer工廠方法。看來.NET實體框架已經離領域驅動設計的思想比較接近了,下面有幾點須要說明:
到這裏你會發現,工廠和倉儲好像有這一種聯繫,即它們都可以建立對象,而區別在於,工廠是對象從無到有的建立,倉儲則更偏向於「重建」。倉儲要比工廠更爲複雜,由於倉儲須要跟持久化機制這一技術架構打交道。在接下來的文章中,我會介紹一種基於.NET實體框架,但又不被實體框架制約的倉儲的實現方式。
模型對象的生命週期 - 倉儲
上文中已經提到了管理領域模型對象生命週期的兩大角色,即工廠與倉儲,並對工廠的EntityFramework實踐做了詳細的描述。本節主要介紹倉儲的概念,因爲倉儲的內容比較多,我將在接下來的兩節中具體講解倉儲的架構設計與實踐經驗。
倉儲(Repository),顧名思義,就是一個倉庫,這個倉庫保存着領域模型的實體對象。在業務處理的過程當中,咱們有可能須要把正在參與處理過程的對象保存到倉儲中,也有可能會從倉儲中讀取須要的實體對象,抑或將對象直接從倉儲中刪除。上文也用一張簡要的狀態圖描述了倉儲在管理領域模型對象生命週期中所處的位置。
與工廠相同,倉儲的關注對象也應該是聚合根,而不是聚合中的某個實體,更不該該是值對象。或許你會說,我固然能夠針對銷售訂單行(Order Line)進行增刪改查等操做,而無需跟銷售訂單(Sales Order)打交道。固然,你的確能夠這樣作,但若是你必定要堅持本身的觀點,那麼你就是把銷售訂單行(Order Line)當成是聚合根了,也就是說,你默許Order Line在你的領域模型中,是一種具備獨立概念的實體。關於這個問題,在領域驅動設計的社區中,有人發表了更爲「強勢」的觀點:
One interesting DDD rule is: you should create repositories only for aggregate roots.
When I read about it the first time I interpreted it this way: create repositories at least for all aggregate roots, but when you need a little repository for something else go ahead and implement it (and nobody will know what you did).
So I was thinking that the rule is somehow flexible. It turns out that it's not, and this is good: it keeps the domain stable and coherent. If entity A is an aggregate root, entity B is part of that aggregate, and you need to load B separated from the concept of A, this is a sign that the implementation does not reflect the business needs (anymore). In this case, B should probably become the root of its own aggregate
意思是說,若是實體A是聚合根,而B是該聚合中的一個實體,而你的設計但願繞過A而直接從倉儲中得到B,那麼,這就是一個信號,預示着你的設計可能存在問題,也就是說,B頗有可能被當成是另外一個聚合的根,而這個聚合只有一個對象,就是B自己。由此看來,聚合的劃分與倉儲的設計,在領域驅動設計的實踐中是很是重要的內容。
工廠是從無到有地建立對象,從代碼上看,工廠裏充斥着new關鍵字,用以建立對象,固然,工廠的職責並不徹底是new出一個對象那麼簡單。而倉儲則更偏向於對象的保存和得到,在得到的時候,一樣也會有新的對象產生,這個新的對象與保存進去的對象相比,引用不一樣了,但數據和業務ID值(也就是咱們常說的實體鍵)是不變的,所以,在領域層看來,從倉儲中讀取獲得的對象與當時保存進去的對象並無什麼兩樣。
你可能已經體會到,倉儲就是一個數據庫,它與數據庫同樣,有讀取、保存、查詢、刪除的操做。我只能說,你已經瞭解到倉儲的職能,並無瞭解到它的角色。倉儲是領域層與基礎結構層的一個銜接組件,領域層經過倉儲訪問外部存儲機制,這樣就使得領域層無需關心任何技術架構上的實現細節。所以,倉儲這個角色的職責不只僅是讀取、保存、查詢、刪除,它還解耦了領域層與基礎結構層。在實踐中,能夠使用依賴注入的方式,將倉儲實例注入到領域層,從而得到靈活的體系結構。
下面是咱們案例中,倉儲接口的代碼:
public interface IRepository<TEntity> where TEntity : EntityObject, IAggregateRoot { void Add(TEntity entity); TEntity GetByKey(int id); IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec); void Remove(TEntity entity); void Update(TEntity entity); } |
IRepository是一個泛型接口,泛型類型被where子句限定爲EntityFramework中的EntityObject,與此同時,where子句還限定了泛型類型必須實現IAggregateRoot接口。換句話講,IRepository接口的泛型類型必須是繼承於EntityObject類,並實現了IAggregateRoot接口的引用類型。根據咱們在 「聚合」一文中的表述,咱們能夠實現針對Customer、Order以及Category實體類的倉儲類。
這裏只給出了倉儲實現的一個引子,至少到目前爲止咱們已經簡單地定義了倉儲實現的一個框架,也就是上面這個IRepository泛型接口。接口中具體要包括哪些方法,不是本系列文章要討論的關鍵問題。爲了描述與演示,咱們只爲IRepository接口設計如上四個方法,即Add、GetByKey、Remove和Update。接下來,我將詳細描述在基於實體框架(EntityFramework)的倉儲設計中所遇到的困難,以及如何在實踐中解決這些困難。
倉儲的實現:基本篇
咱們先從技術角度考慮倉儲的問題。實體框架(EntityFramework)中,操做數據庫是很是簡單的:在ObjectContext中使用LINQ to Entities便可完成操做。開發人員也不須要爲事務管理而操心,一切都由EF包辦。與本來的ADO.NET以及LINQ to SQL相比,EF更爲簡單,LINQ to Entities的引入使得軟件開發變得更爲「領域化」。
下面的代碼測試了持久化一個 Customer實體,並從持久化機制中查詢這個Customer實體的正確性。從代碼中能夠看到,咱們用了一種很天然的表達方式,表述了「我但願查詢一個名字爲Sunny的客戶」這樣一種業務邏輯。
[TestMethod] public void FindCustomerTest() { Customer customer = Customer.CreateCustomer("daxnet", "12345", new Name { FirstName = "Sunny", LastName = "Chen" }, new Address(), new Address(), DateTime.Now.AddYears(-29)); using (EntitiesContainer ec = new EntitiesContainer()) { ec.Customers.AddObject(customer); ec.SaveChanges(); } using (EntitiesContainer ec = new EntitiesContainer()) { var query = from cust in ec.Customers where cust.Name.FirstName.Equals("Sunny") select cust; Assert.AreNotEqual(0, query.Count()); } } |
若是你須要實現的系統並不複雜,那麼按上面的方式添加、查詢實體也不會有太大問題,你能夠在 ObjectContext中爲所欲爲地使用LINQ to Entities來方便地獲得你須要的東西,更讓人興奮的是,.NET 4.0容許支持並行計算的PLINQ,若是你的計算機具備多核處理器,你將很是方便地得到效率上的提高。然而,當你的架構須要考慮下面幾個方面時,單純的 LINQ to Entities方式就沒法知足需求了:
因而,也就回到了上篇博客中我描述的問題:倉儲不是Data Object,也不只僅是進行數據庫CRUD操做的Data Manager,它承擔瞭解耦領域模型和技術架構的重要職責。爲了完美地解決上面提到的問題,咱們仍然採用領域驅動設計中倉儲的設計模式,而將實體框架做爲倉儲的具體實現部分。在詳細介紹倉儲的設計與實現以前,讓咱們回顧一下上文最後部分我提到的那個倉儲的接口:
public interface IRepository<TEntity> where TEntity : EntityObject, IAggregateRoot { void Add(TEntity entity); TEntity GetByKey(int id); IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec); void Remove(TEntity entity); void Update(TEntity entity); } |
在本文的案例中,倉儲是這樣實現的:
1.將上述倉儲接口定義在實體、值對象和服務所在的領域層。有朋友問過我,既然倉儲須要與外部存儲機制打交道,那麼它一定須要知道技術架構方面的細節,而將其定義在領域層,就會使得領域層依賴於具體的技術實現方式,這樣就會使領域層變得「不純淨」 了。其實否則!請注意,咱們這裏僅僅只是將倉儲的接口定義在了領域層,而不是倉儲的具體實現(Concrete Repository)。更通俗地說,接口做爲系統架構的基礎元素,決定了整個系統的架構模式,而基於接口的具體實現只不過是一種可替換的組件,它不能成爲系統架構中的一部分。因爲領域層須要用到倉儲,我便將倉儲的接口定義在了領域層。固然,從.NET的實現技術考慮,你能夠新建一個Class Library,並將上述接口定義在這個Class Library中,而後在領域層和倉儲的具體實現中分別引用這個Class Library
2.新建一個Class Library(在本文的案例中,命名爲EasyCommerce.Infrastructure.Repositories),添加對領域層 assembly的引用,並實現上述接口。因爲咱們採用實體框架做爲倉儲的具體實現,所以,將這個倉儲命名爲EdmRepository(Entity Data Model Repository)。EdmRepository有着相似以下的實現:
internal class EdmRepository<TEntity> : IRepository<TEntity> where TEntity : EntityObject, IAggregateRoot { #region Private Fields private readonly ObjectContext objContext; private readonly string entitySetName; #endregion #region Constructors /// <summary> /// /// </summary> /// <param name="objContext"></param> public EdmRepository(ObjectContext objContext) { this.objContext = objContext; if (!typeof(TEntity).IsDefined(typeof(AggregateRootAttribute), true)) throw new Exception(); AggregateRootAttribute aggregateRootAttribute = (AggregateRootAttribute)typeof(TEntity) .GetCustomAttributes(typeof(AggregateRootAttribute), true)[0]; this.entitySetName = aggregateRootAttribute.EntitySetName; } #endregion #region IRepository<TEntity> Members public void Add(TEntity entity) { this.objContext.AddObject(EntitySetName, entity); } public TEntity GetByKey(int id) { string eSql = string.Format("SELECT VALUE ent FROM {0} AS ent WHERE ent.Id=@id", EntitySetName); var objectQuery = objContext.CreateQuery<TEntity>(eSql, new ObjectParameter("id", id)); if (objectQuery.Count() > 0) return objectQuery.First(); throw new Exception("Not found"); } public void Remove(TEntity entity) { this.objContext.DeleteObject(entity); } public void Update(TEntity entity) { // TODO } public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec) { throw new NotImplementedException(); } #endregion #region Protected Properties protected string EntitySetName { get { return this.entitySetName; } } protected ObjectContext ObjContext { get { return this.objContext; } } #endregion } |
從上面的代碼能夠看到,EdmRepository將實體框架抽象到ObjectContext這一層,這也使咱們無法經過LINQ to Entities來查詢模型中的對象。幸運的是,ObjectContext爲咱們提供了一系列函數,用以實現實體的CRUD。爲了使用這些函數,咱們須要知道與實體相關的EntitySetName,爲此,我定義了一個AggregateRootAttribute,並將其應用在聚合根上,以便在對實體進行操做的時候,可以正確地得到EntitySetName。相似的代碼以下:
[AggregateRoot("Customers")] partial class Customer : IAggregateRoot { } |
回頭來看EdmRepository的構造函數,在構造函數中,咱們使用.NET的反射機制得到了定義在聚合根類型上的EntitySetName
3. 使用IoC/DI(控制反轉/依賴注入)框架,將倉儲的實現(EdmRepository)注射到領域模型中。至此,領域模型一直保持着對倉儲接口的引用,而對倉儲的具體實現方式一無所知。因爲IoC/DI的引入,咱們獲得了一個純淨的領域模型。在這裏我也想提出一個衡量系統架構優劣度的重要指標,就是領域模型的純淨度。常見的 IoC/DI框架有Spring.NET和Castle Windsor MicroKernel。在本文的案例中,我採用了Castle Windsor。如下是針對Castle Windsor的配置文件片斷:
<castle> <components> <!-- Object Context for Entity Data Model --> <component id="ObjectContext" service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0, Culture=neutral, type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/> <component id="CustomerRepository" service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, EasyCommerce.Doma type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCommerce.Doma <objContext>${ObjectContext}</objContext> </component> </components> </castle> |
經過這個配置片斷咱們還能夠看到,在框架建立針對「客戶」實體的倉儲實例時,咱們案例中的領域模型容器(EntitiesContainer)也以構造器注入的方式,被注射到了EdmRepository的構造函數中。接下來咱們作一個單體測試:
考察上面的代碼,倉儲的使用者(Client,能夠是領域模型中的任何對象)對倉儲的具體實現一無所知
總結
總之,倉儲的實現能夠用下圖表述:
回頭來看本文剛開始的三個問題:依賴注入能夠解決問題1和3,而倉儲接口的引入,也使得規約模式的應用成爲可能。.NET中有一個泛型委託,稱爲Func<T, bool>,它能夠做爲LINQ的where子句參數,實現相似規約的功能。有關規約模式,我將在其它的文章中討論。
從本文還能夠了解到,依賴注入是維持領域模型純淨度的一大利器;另外一大利器是領域事件,我將在後續的文章中詳述。對於本文開始的第三個問題,也就是倉儲實現的可擴展性,將在下篇文章中進行討論,包括的內容有:事務處理和可擴展的倉儲框架的實現。
倉儲的實現:深刻篇
早在年前的時候就已經在CSAI博客發表了上一篇文章:《倉儲的實現:基礎篇》。苦於日夜奔波於工做與生活之間,一直沒有可以抽空繼續探討倉儲的實現細節,也讓不少關注EntityFramework和領域驅動設計的朋友們備感失望。
閒話很少說,如今繼續考慮,如何讓倉儲的操做在相同的事物處理上下文中進行。DDD引入倉儲模式,其目的之一就是可以經過倉儲隱藏對象持久化的技術細節,使得領域模型變得更爲「純淨」。因而可知,倉儲的實現是須要基礎結構層的組件支持的,表現爲對數據庫的操做。在傳統的關係型數據庫操做中,事務處理是一個很重要的概念,雖然從目前某些大型項目看,事務處理會下降效率,但它保證了數據的完整性。關係型數據庫仍然是目前數據持久化機制的主流,事務處理的實現仍是頗有必要的。
爲了迎合倉儲模式,就須要對經典的ObjectContext使用方式做一些調整。好比,本來咱們能夠很是簡單地使用using (EntitiesContainer ec = new EntitiesContainer())語句來界定LINQ to Entities的操做範圍,並使用ObjectContext的SaveChanges成員方法提交事務,而在引入了倉儲的實現中,就不能繼續採用這種經典的使用方式。這讓EntityFramework看上去變得很奇怪,也很牽強,我相信不少網友會批評個人作法,由於我把問題複雜化了。
其實,這應該是關注點不一樣罷了。關注EntityFramework的開發人員,天然以爲經典的調用方式簡單明瞭,而從DDD的角度看呢?只能把關注點放在倉儲上,而把EntityFramework當成是倉儲的一種技術選型(固然從DDD角度講,咱們徹底能夠不選擇EntityFramework,而去選擇其它技術)。因此本文暫且拋開EntityFramework,繼續在上文的基礎上,討論倉儲的實現。
前面提到,倉儲的實現須要考慮事務處理,並且根據DDD的經驗,針對每個聚合根,都須要有個倉儲對其進行持久化以及對象從新組裝等操做。爲此,個人想法是,將倉儲操做「界定」在某一個事務處理上下文(Context)中,倉儲的實例是由Context得到的,這有點像EntityFramework中ObjectContext與EntityObject的關係那樣。因爲倉儲是來自於transaction context,因此它知道目前處於哪一個事務上下文中。我定義的這個transaction context以下:
public interface IRepositoryTransactionContext : IDisposable { IRepository<TEntity> GetRepository<TEntity>() where TEntity : EntityObject, IAggregateRoot; void BeginTransaction(); void Commit(); void Rollback(); } |
上面第三行代碼定義了一個接口方法,這個方法的主要做用就是返回一個針對指定聚合根實體的倉儲實例。剩下那三行代碼就很明顯了,那是標準的transaction操做:啓動事務、提交事務以及回滾事務。
在設計上,能夠根據須要,選擇合適的技術來實現IRepositoryTransactionContext。咱們如今討論的是EntityFramework,因此我將給出EntityFramework的具體實現。固然,若是你不選用EntityFramework,而是用NHibernate實現數據持久化,這樣的設計一樣可以使你達到目的。如下是基於EntityFramework的實現:EdmRepositoryTransactionContext的僞代碼。
internal class EdmRepositoryTransactionContext : IRepositoryTransactionContext { private ObjectContext objContext; private Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>(); public EdmRepositoryTransactionContext(ObjectContext objContext) { this.objContext = objContext; } #region IRepositoryTransactionContext Members public IRepository<TEntity> GetRepository<TEntity>() where TEntity : EntityObject, IAggregateRoot { if (repositoryCache.ContainsKey(typeof(TEntity))) { return (IRepository<TEntity>)repositoryCache[typeof(TEntity)]; } IRepository<TEntity> repository = new EdmRepository<TEntity>(this.objContext); this.repositoryCache.Add(typeof(TEntity), repository); return repository; } public void BeginTransaction() { // We do not need to begin a transaction here because the object context, // which would handle the transaction, was created and injected into the // constructor by Castle Windsor framework. } public void Commit() { this.objContext.SaveChanges(); } public void Rollback() { // We also do not need to perform the rollback operation because // entity framework will handle this for us, just when the execution // point is stepping out of the using scope. } #endregion #region IDisposable Members public void Dispose() { this.repositoryCache.Clear(); this.objContext.Dispose(); } #endregion } |
EdmRepositoryTransactionContext被定義爲internal,這個設計是合理的,由於Domain層是不須要知道事務上下文的具體實現,它將會被IoC/DI容器注入到Domain層中(本系列文章採用Castle Windsor框架)。在EdmRepositoryTransactionContext的構造函數中,它須要EntityFramework的ObjectContext對象來初始化實例。一樣,因爲IoC/DI的使用,咱們在代碼中也是不須要去建立這個ObjectContext的,交給Castle Windsor就OK了。第13行的GetRepository方法簡單地採用了Dictionary對象來實現緩存倉儲實例的效果,固然這種作法還有待改進。
EdmRepositoryTransactionContext是不須要BeginTransaction的,咱們將方法置空,由於EntityFramework的事務會由ObjectContext來管理,同理,Rollback也被置空。
EdmRepository的實現就比較顯而易見了,請參見上文。
此外,咱們還能夠針對NHibernate實現倉儲模式,只須要實現IRepositoryTransactionContext和IRepository接口便可,好比:
internal class NHibernateRepositoryTransactionContext : IRepositoryTransactionContext { ITransaction transaction; Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>(); public ISession Session { get { return DatabaseSessionFactory.Instance.Session; } } #region IRepositoryTransactionContext Members public IRepository<TEntity> GetRepository<TEntity>() where TEntity : EntityObject, IAggregateRoot { if (repositoryCache.ContainsKey(typeof(TEntity))) { return (IRepository<TEntity>)repositoryCache[typeof(TEntity)]; } IRepository<TEntity> repository = new NHibernateRepository<TEntity>(this.Session); this.repositoryCache.Add(typeof(TEntity), repository); return repository; } public void BeginTransaction() { transaction = DatabaseSessionFactory.Instance.Session.BeginTransaction(); } public void Commit() { transaction.Commit(); } public void Rollback() { transaction.Rollback(); } #endregion #region IDisposable Members public void Dispose() { transaction.Dispose(); ISession dbSession = DatabaseSessionFactory.Instance.Session; if (dbSession != null && dbSession.IsOpen) dbSession.Close(); } #endregion } |
internal class NHibernateRepository<TEntity> : IRepository<TEntity> where TEntity : EntityObject, IAggregateRoot { ISession session; public NHibernateRepository(ISession session) { this.session = session; } #region IRepository<TEntity> Members public void Add(TEntity entity) { this.session.Save(entity); } public TEntity GetByKey(int id) { return (TEntity)this.session.Get(typeof(TEntity), id); } public IEnumerable<TEntity> FindBySpecification(Func<TEntity, bool> spec) { throw new NotImplementedException(); } public void Remove(TEntity entity) { this.session.Delete(entity); } public void Update(TEntity entity) { this.session.SaveOrUpdate(entity); } #endregion } |
在NHibernateRepositoryTransactionContext中使用了一個DatabaseSessionFactory的類,該類主要用於管理NHibernate的Session對象,具體實現以下(該實現已被用於個人Apworks應用開發框架原型中):
/// <summary> /// Represents the factory singleton for database session. /// </summary> internal sealed class DatabaseSessionFactory { #region Private Static Fields /// <summary> /// The singleton instance of the database session factory. /// </summary> private static readonly DatabaseSessionFactory databaseSessionFactory = new DatabaseSessionFactory(); #endregion #region Private Fields /// <summary> /// The session factory instance. /// </summary> private ISessionFactory sessionFactory = null; /// <summary> /// The session instance. /// </summary> private ISession session = null; #endregion #region Constructors /// <summary> /// Privately constructs the database session factory instance, configures the /// NHibernate framework by using the assemblies listed in the configured spaces(paths) /// that are decorated by <see cref="EntityVisibleAttribute"/>. /// </summary> private DatabaseSessionFactory() { Configuration nhibernateConfig = new Configuration(); nhibernateConfig.Configure(); nhibernateConfig.AddAssembly(typeof(IAggregateRoot).Assembly); sessionFactory = nhibernateConfig.BuildSessionFactory(); } #endregion #region Public Properties /// <summary> /// Gets the singleton instance of the database session factory. /// </summary> public static DatabaseSessionFactory Instance { get { return databaseSessionFactory; } } /// <summary> /// Gets the singleton instance of the session. If the session has not been /// initialized or opened, it will return a newly opened session from the session factory. /// </summary> public ISession Session { get { ISession result = session; if (result != null && result.IsOpen) return result; return OpenSession(); } } #endregion #region Public Methods /// <summary> /// Always opens a new session from the session factory. /// </summary> /// <returns>The newly opened session.</returns> public ISession OpenSession() { this.session = sessionFactory.OpenSession(); return this.session; } #endregion } |
簡單小結一下。經過上面的例子能夠看到,倉儲的實現是不能依賴於任何技術細節的,由於領域模型並不關心技術問題。這並非DDD一書中怎麼說,咱們就得怎麼作。事實上的確如此,由於DDD的思想,使得咱們應該把關注點放在業務分析與領域建模上,而倉儲實現的分離正是這一思想的具體表現形式。無論怎麼樣,採用其它的手段也罷,咱們仍是應該遵循「將關注點放在領域」這一宗旨。
接下來看如何在領域層結合IoC框架使用倉儲。仍然以Castle Windsor爲例。配置文件以下(超長部分我用省略號去掉了):
<castle> <components> <!-- Object Context for Entity Data Model --> <component id="ObjectContext" service="System.Data.Objects.ObjectContext, System.Data.Entity, Version=4.0.0.0,......" type="EasyCommerce.Domain.Model.EntitiesContainer, EasyCommerce.Domain"/> <component id="GeneralRepository" service="EasyCommerce.Domain.IRepository`1[[EasyCommerce.Domain.Model.Customer, ......" type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepository`1[[EasyCo......"> <objContext>${ObjectContext}</objContext> </component> <component id="TransactionContext" service="EasyCommerce.Domain.IRepositoryTransactionContext, EasyCommerce.Domain......" type="EasyCommerce.Infrastructure.Repositories.EdmRepositories.EdmRepositoryTransactionContext, ..."> </component> </components> </castle> |
如下是調用代碼:
[TestMethod] public void TestCreateCustomer() { IWindsorContainer container = new WindsorContainer(new XmlInterpreter()); using (IRepositoryTransactionContext tx = container.GetService<IRepositoryTransactionContext>()) { tx.BeginTransaction(); try { Customer customer = Customer.CreateCustomer("daxnet", "12345", new Name { FirstName = "Sunny", LastName = "Chen" }, new Address(), new Address(), DateTime.Now.AddYears(-29)); IRepository<Customer> customerRepository = tx.GetRepository<Customer>(); customerRepository.Add(customer); tx.Commit(); } catch { tx.Rollback(); throw; } } } |
測試結果及數據庫數據結果:
【注意】:在使用NHibernate的倉儲實現時,因爲NHibernate的延遲加載特性,須要將實體的屬性設置爲virtual,以便由NHibernate產生Proxy Class進而實現延遲加載;可是由EntityFramework自動生成的源代碼並不會將實體屬性設置爲virtual,而採用partial class也沒法解決這個問題。所以須要在代碼生成技術上作文章。個人作法是,針對edmx產生一個基於T4的代碼生成模板,而後修改這個模板,分別在WritePrimitiveTypeProperty和WriteComplexTypeProperty方法中的適當位置加上virtual關鍵字:
private void WritePrimitiveTypeProperty(EdmProperty primitiveProperty, CodeGenerationTools code) { MetadataTools ef = new MetadataTools(this); #> /// <summary> /// <#=SummaryComment(primitiveProperty)#> /// </summary><#=LongDescriptionCommentElement(primitiveProperty, 1)#> [EdmScalarPropertyAttribute(EntityKeyProperty=<#=code.CreateLiteral(ef.IsKey(primitiveProperty))#>, IsNullable=<#=code.CreateLiteral(ef.IsNullable(primitiveProperty))#>)] [DataMemberAttribute()] <#=code.SpaceAfter(NewModifier(primitiveProperty))#><#=Accessibility.ForProperty(primitiveProperty)#> virtual <#=code.Escape(primitiveProperty.TypeUsage)#> <#=code.Escape(primitiveProperty)#> { <#=code.SpaceAfter(Accessibility.ForGetter(primitiveProperty))#>get { <#+ if (ef.ClrType(primitiveProperty.TypeUsage) == typeof(byte[])) { #> return StructuralObject.GetValidValue(<#=code.FieldName(primitiveProperty)#>); |
private void WriteComplexTypeProperty(EdmProperty complexProperty, CodeGenerationTools code) { #> /// <summary> /// <#=SummaryComment(complexProperty)#> /// </summary><#=LongDescriptionCommentElement(complexProperty, 1)#> [EdmComplexPropertyAttribute()] [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] [XmlElement(IsNullable=true)] [SoapElement(IsNullable=true)] [DataMemberAttribute()] <#=code.SpaceAfter(NewModifier(complexProperty))#><#=Accessibility.ForProperty(complexProperty)#> virtual <#=MultiSchemaEscape(complexProperty.TypeUsage, code)#><#=code.Escape(complexProperty)#> { <#=code.SpaceAfter(Accessibility.ForGetter(complexProperty))#>get { <#=code.FieldName(complexProperty)#> = GetValidValue(<#=code.FieldName(complexProperty)#>, "<#=complexProperty.Name#>", false, <#=InitializedTrackingField(complexProperty, code)#>); <#=InitializedTrackingField(complexProperty, code)#> = true; |
始終堅持一個原則:不要在生成的代碼上直接修改,一是工做量巨大,另外一方面就是,代碼是自動生成的,從此模型修改了,代碼將會從新生成。
規約(Specification)模式
原本針對規約模式的討論,我並無想將其列入本系列文章,由於這是一種概念性的東西,從理論上講,與EntityFramework好像扯不上關係。但應廣大網友的要求,我決定仍是在這裏討論一下規約模式,並介紹一種專門針對.NET Framework的規約模式實現。
不少時候,咱們都會看到相似下面的設計:
public interface ICustomerRespository { Customer GetByName(string name); Customer GetByUserName(string userName); IList<Customer> GetAllRetired(); } |
接下來的一步就是實現這個接口,並在類中分別實現接口中的方法。很明顯,在這個接口中,Customer倉儲一共作了三個操做:經過姓名獲取客戶信息;經過用戶名獲取客戶信息以及得到全部當前已退休客戶的信息。這樣的設計有一個好處就是一目瞭然,可以很方便地看到Customer倉儲到底提供了哪些功能。文檔化的開發方式特別喜歡這樣的設計。
仍是那句話,應需而變。若是你的系統很簡單,而且從此擴展的可能性不大,那麼這樣的設計是簡潔高效的。但若是你正在設計一箇中大型系統,那麼,下面的問題就會讓你感到困惑:
規約模式就是DDD引入用來解決以上問題的一種特殊的模式。規約是一種布爾斷言,它表述了給定的對象是否知足當前約定的語義。經典的規約模式實現中,規約類只有一個方法,就是IsSatisifedBy(object);以下:
public class Specification { public virtual bool IsSatisifedBy(object obj) { return true; } } |
仍是先看例子吧。在引入規約之後,上面的代碼就能夠修改成:
public interface ICustomerRepository { Customer GetBySpecification(Specification spec); IList<Customer> GetAllBySpecification(Specification spec); } public class NameSpecification : Specification { protected string name; public NameSpecification(string name) { this.name = name; } public override bool IsSatisifedBy(object obj) { return (obj as Customer).FirstName.Equals(name); } } public class UserNameSpecification : NameSpecification { public UserNameSpecification(string name) : base(name) { } public override bool IsSatisifedBy(object obj) { return (obj as Customer).UserName.Equals(this.name); } } public class RetiredSpecification : Specification { public override bool IsSatisifedBy(object obj) { return (obj as Customer).Age >= 60; } } public class Program1 { static void Main(string[] args) { ICustomerRepository cr; // = new CustomerRepository(); Customer getByNameCustomer = cr.GetBySpecification(new NameSpecification("Sunny")); Customer getByUserNameCustomer = cr.GetBySpecification(new UserNameSpecification("daxnet")); IList<Customer> getRetiredCustomers = cr.GetAllBySpecification(new RetiredSpecification()); } } |
經過使用規約,咱們將Customer倉儲中全部「特定用途的操做」所有去掉了,取而代之的是兩個很是簡潔的方法:分別經過規約來得到Customer實體和實體集合。規約模式解耦了倉儲操做與斷言條件,從此咱們須要經過倉儲實現其它特定條件的查詢時,只須要定製咱們的Specification,並將其注入倉儲便可,倉儲的實現無需任何修改。與此同時,規約的引入,使得咱們很清晰地瞭解到,某一次查詢過濾,或者某一次數據校驗是以什麼樣的規則實現的,這給斷言條件的設計與實現帶來了可測試性。
爲了實現複合斷言,一般在設計中引入複合規約對象。這樣作的好處是,能夠充分利用規約的複合來實現複雜的規約組合以及規約樹的遍歷。不只如此,在.NET 3.5引入Expression Tree之後,規約將有其特定的實現方式,這個咱們在後面討論。如下是一個經典的實現方式,注意ICompositeSpecification接口,它包含兩個屬性:Left和Right,ICompositeSpecification是繼承於ISpecification接口的,而Left和Right自己也是ISpecification類型,因而,整個Specification的結構就能夠當作是一種樹狀結構。
還記得在《EntityFramework之領域驅動設計實踐(八)- 倉儲的實現:基本篇》裏提到的倉儲接口設計嗎?當初尚未牽涉到任何Specification的概念,因此,倉儲的FindBySpecification方法採用.NET的Func<TEntity, bool>委託做爲Specification的聲明。如今咱們引入了Specification的設計,因而,倉儲接口能夠改成:
public interface IRepository<TEntity> where TEntity : EntityObject, IAggregateRoot { void Add(TEntity entity); TEntity GetByKey(int id); IEnumerable<TEntity> FindBySpecification(ISpecification spec); void Remove(TEntity entity); void Update(TEntity entity); } |
針對規約模式實現的討論,咱們纔剛剛開始。如今,又出現了下面的問題:
基於.NET的Specification能夠使用LINQ Expression(下面簡稱Expression)來解決上面全部的問題。爲了引入Expression,咱們須要對ISpecification的設計作點點修改。代碼以下:
public interface ISpecification { bool IsSatisfiedBy(object obj); Expression<Func<object, bool>> Expression { get; } // Other member goes here... } public abstract class Specification : ISpecification { #region ISpecification Members public bool IsSatisfiedBy(object obj) { return this.Expression.Compile()(obj); } public abstract Expression<Func<object, bool>> Expression { get; } #endregion } |
僅僅引入一個Expression<Func<object, bool>>屬性,就解決了上面的問題。在實際應用中,咱們實現Specification類的時候,由原來的「實現IsSatisfiedBy方法」轉變爲「實現Expression<Func<object, bool>>屬性」。如今主流的.NET對象持久化機制(好比EntityFramework,NHibernate,Db4o等等)都支持LINQ接口,因而:
public abstract class Specification : ISpecification { // ISpecification implementation omitted public static ISpecification Eval(Expression<Func<object, bool>> expression) { return new ExpressionSpec(expression); } } internal class ExpressionSpec : Specification { private Expression<Func<object, bool>> exp; public ExpressionSpec(Expression<Func<object, bool>> expression) { this.exp = expression; } public override Expression<Func<object, bool>> Expression { get { return this.exp; } } } class Client { static void CallSpec() { ISpecification spec = Specification.Eval(o => (o as Customer).UserName.Equals("daxnet")); // spec.... } } |
下圖是基於LINQ Expression的Specification設計的完整類圖。與經典Specification模式的實現相比,除了LINQ Expression的引入外,本設計中採用了IEntity泛型約束,用於將Specification的操做約束在領域實體上,同時也提供了強類型支持。
上圖的右上角有個ISpecificationParser的接口,它主要用於將Specification解析爲某一持久化框架能夠認識的對象,好比LINQ Expression或者NHibernate的Criteria。固然,在引入LINQ Expression的Specification中,這個接口是能夠不去實現的;而對於NHibernate,咱們能夠藉助NHibernate.Linq命名空間來實現這個接口,從而將Specification轉換爲NHibernate Criteria。相關代碼以下:
internal sealed class NHibernateSpecificationParser : ISpecificationParser<ICriteria> { ISession session; public NHibernateSpecificationParser(ISession session) { this.session = session; } #region ISpecificationParser<Expression> Members public ICriteria Parse<TEntity>(ISpecification<TEntity> specification) where TEntity : class, IEntity { var query = this.session.Linq<TEntity>().Where(specification.GetExpression()); //Expression<Func<TEntity, bool>> exp = obj => specification.IsSatisfiedBy(obj); //var query = this.session.Linq<TEntity>().Where(exp); System.Linq.Expressions.Expression expression = query.Expression; expression = Evaluator.PartialEval(expression); expression = new BinaryBooleanReducer().Visit(expression); expression = new AssociationVisitor((ISessionFactoryImplementor)this.session.SessionFactory) .Visit(expression); expression = new InheritanceVisitor().Visit(expression); expression = CollectionAliasVisitor.AssignCollectionAccessAliases(expression); expression = new PropertyToMethodVisitor().Visit(expression); expression = new BinaryExpressionOrderer().Visit(expression); NHibernateQueryTranslator translator = new NHibernateQueryTranslator(this.session); var results = translator.Translate(expression, ((INHibernateQueryable)query).QueryOptions); ICriteria ca = results as ICriteria; return ca; } #endregion } |
其實,Specification相關的話題遠不止本文所討論的這些,更多內容須要咱們在實踐中發掘、思考。本文也只是對規約模式及其在.NET中的實現做了簡要的討論,文中也會存在欠考慮的地方,歡迎各位網友各抒己見,提出寶貴意見。
EntityFramework之領域驅動設計實踐【擴展閱讀】:服務(Services)
服務(Services)
從本講開始,所涉及的DDD話題可能與EntityFramework關係不大了。網友千萬別罵我是標題黨,呵呵。因爲這部份內容並不是是特定於EntityFramework的,更多的是在介紹模式及實踐心得,因此EntityFramework的內容就會偏少了。爲了使得針對一些話題的討論可以延續下去,我仍然將這些文章安排在本系列中,但願讀者朋友可以諒解。我也在標題中標註了【擴展閱讀】,表示所討論的內容已經不只僅侷限於EntityFramework了。
爲了表示補償,透露一下EntityFramework 4.0的最新特性:EF CTP 4.0在「代碼優先」開發模式以及提高開發生產率方面作了重要改進。EF CTP 4.0引入了兩種新的類型:DbContext和DbSet。DbContext是ObjectContext的簡化版。詳情請見http://www.infoq.com/news/2010/07/EF-CTP-4。
言歸正傳,本文將對DDD中的又一重要角色:服務(Services)作一些簡單的介紹。提起服務,不少朋友都會想到「SOA」。而在領域驅動設計裏,服務貫穿於整個系統的各個層面。根據應用系統的領域驅動分層思想,服務被歸類爲:應用層服務、領域服務以及基礎結構層服務。應用層服務爲表現層提供接口,根據DDD的思想,應用層很薄,不承擔任何業務邏輯的處理,僅僅是起到coordination的做用。所以,應用層服務也不會牽涉到業務邏輯。在CQRS模式中,Command Service以及Query Service就是應用層服務。基礎結構層服務是顯而易見的,好比,郵件發送服務、數據服務、事件總線等等。這些服務是與領域無關的,只跟技術實現相關。假想這樣一個例子:將貨物從倉庫A轉移到倉庫B,若是轉倉成功,則向倉庫管理員及操做員發送SMS。這是倉儲管理領域常見的業務需求,經典的寫法相似以下:
public class TransferService : IDomainService { public void Transfer(Warehouse a, Warehouse b, Item item, Qty qty) { using (IRepositoryTransactionContext ctx = Ioc.GetService()) { Inventory oItemInA = a.GetInventory(item); if (oItemInA.Qty < qty) { // raise not enough inventory event or exception throw new Exception(); } Inventory oItemInB = b.GetInventory(item); if (oItemInB == null) oItemInB = b.CreateInventory(item); oItemInA.Qty -= qty; oItemInB.Qty += qty; ctx.SaveChanges(); } } } |
在上面的僞代碼中,咱們已經看到了領域服務(Domain Service)的影子。在DDD裏,領域服務用以處理那種「放在哪裏都不合適」的業務邏輯。好比上面的轉倉業務,從面向對象的角度看,既不是倉庫應有的操做,也不是貨物(Item)的行爲。爲了明確領域對象的職責,DDD將這樣的業務邏輯「抽」出來,置於領域服務當中。對於發送SMS的需求,就須要由應用層服務經過「協調」進行處理了。好比:在調用了領域服務並得到響應之後,根據響應結果以及外部配置信息,進而調用基礎結構層服務(SMSService)實現SMS的發送。
看到這裏你會發現,其實哪些業務應該放在實體中,哪些須要使用服務來處理,並無一個絕對的標準。仍是那句老話:憑經驗。你還會發現,若是從實體將業務邏輯所有「抽」到服務裏,實體將成爲僅包含getter/setter的對象,因而貧血模型產生了。正由於DDD提倡面向領域,並將通用語言和領域模型擺在很重要的位置,所以,DDD是不主張貧血模型的。
我的認爲,領域服務的引入,增長了模型的抗需求變動的能力。咱們能夠經過需求分析,找出業務邏輯中易變的部分,以領域服務的方式「注入」到領域模型中,從此如有需求變動,則能夠無需更改任何現有組件,完成業務處理邏輯的改變。[TBD: 這樣的想法還有待進一步證明]
有關領域服務的內容,本文暫且討論這些。讀者朋友能夠在實踐中提出問題,而後在此與你們分享討論。本文還引出了一個話題,就是應用層服務的協調問題。好比,本文的例子中,是在應用層服務中調用SMSService實現SMS發送,若是直接將這部份內容寫在應用層服務中,勢必下降系統的擴展性。好比,從此但願不只要發送SMS,並且還要發送Email。DDD的解決方案是引入事件模型。在完成轉倉操做時,向事件總線(Event Bus)發送事件,由事件訂閱者Subscriber捕獲並處理(Handle)事件。因而,從此咱們只須要實現一個「WarehouseTransferSendEmailEventHandler」的事件處理器,並在Handle Event的調用中,向相關人員發送Email便可。NServiceBus就是一款經典的基於.NET的企業級應用通訊的框架,在基於事件的DDD架構中,NServiceBus發揮了重要做用。
從下一講開始,我將着重討論領域事件以及Event Sourcing,並對DDD的CQRS模式做個引子。
EntityFramework之領域驅動設計實踐【擴展閱讀】:CQRS體系結構模式
CQRS體系結構模式
本文將對CQRS(Command Query Responsibility Segregation,命令查詢職責分離)模式作一個相對全面的介紹。能夠這麼說,CQRS打破了經典的領域驅動設計實踐,在應用CQRS的整個過程當中,你將會以另外一種不一樣的角度去考慮問題並尋求解決方案。好比,CQRS是事件驅動的體系結構,事件是如何產生如何分發又是如何處理的?事件驅動的體系結構適用於哪些類型的應用系統?CQRS中的倉儲,與經典DDD中的倉儲又有何異同?等等這些問題,都給咱們留下了無限的思考空間。
背景
在講CQRS以前,咱們先了解一下CQS(Command-Query Separation,命令查詢)模式。名字上看,二者沒什麼差異,然而CQRS應該說是,在DDD的實踐中引入CQS理論而出現的一種體系結構模式。CQS模式最先由著名軟件大師Bertrand Meyer(Eiffel語言之父,面向對象開-閉原則OCP提出者)提出,他認爲,對象的行爲僅有兩種:命令和查詢,不存在第三種狀況。用他本身的話來講,就是:「提問永遠沒法改變答案」。根據CQS,任何方法均可以拆分爲命令和查詢兩個部分。好比,下面的代碼:
private int i = 0; private int Add(int factor) { i += factor; return i; } |
能夠替換爲:
private void AddCommand(int factor) { i += factor; } private int QueryValue() { return i; } |
當命令和查詢被分離的時候,咱們將會有更多的機會去把握整個事情的細節。好比咱們能夠對系統的「命令」部分和「查詢」部分分別採用不一樣的技術架構,以使得系統具備更好的擴展性,並得到更好的性能。在DDD領域中,Greg Young和Eric Evans根據Bertrand Meyer的CQS模式,結合實際項目經驗,總結了CQRS體系結構模式。
結構
整個系統結構被分爲兩個部分:命令部分和查詢部分。我根據本身的體會,描繪了CQRS的體系結構簡圖以下,供你們參考。在討論CQRS體系結構以前,咱們有必要事先弄清楚這樣幾個概念:對象狀態、事件溯源(Event Sourcing)、快照(Snapshots)以及事件存儲(Event Store)。討論的過程當中你會發現,不少概念與咱們以前對經典DDD的理解相比,有着很大的不一樣。
對象狀態
這是一個你們耳熟能詳的概念了。什麼是對象狀態?在被面向對象編程(OOP)「薰陶」了好久的朋友,一聽到「對象狀態」,立刻想到了一對對的getter/setter屬性。尤爲是.NET程序員,在C# 3.0及之後版本中,引入了Auto-Property的概念,因而,對象的屬性就很容易地成爲了對象狀態的代名詞。在這裏,咱們應該看到問題的本質,即便是Auto-Property,它也無非是對對象字段的一種封裝,只不過在使用Auto-Property的時候,C#編譯器會在後臺建立一個私有的、匿名的字段(field),而Property則成爲了從外部訪問該字段的惟一途徑。換句話說,對象的狀態是保存在這些字段裏的,對象屬性無非是訪問字段的facade。在這裏澄清這樣一個事實,就是爲了當你繼續閱讀本文的時候,不至於對事件溯源(Event Sourcing)的某些具體實現感到困惑。在Event Sourcing的具體實現中,領域對象再也不須要具有公有的屬性,至少外界沒法經過公有屬性改變對象狀態(即setter被定義爲private,甚至沒有setter)。這與經典的DDD設計相比,無疑是一個重大改變。例如,如今我要改變某個Customer的狀態,若是採用經典DDD的實現方式,就是:
[TestMethod] public void TestChangeCustomerName() { IocContainer c = IocContainer.GetIocContainer(); using (IRepositoryTransactionContext ctx = c.GetService<IRepositoryTransactionContext>()) { IRepository<Customer> customerRepository = ctx.GetRepository<Customer>(); Customer customer = customerRepository .Get(Specification<Customer> .Eval(p=>p.FirstName.Equals("sunny") && p.LastName.Equals("chen"))); // Here we use the properties directly to update the state customer.FirstName = "dax"; customer.LastName = "net"; customerRepository.Update(customer); ctx.Commit(); } } |
如今,不少ORM工具都須要聚合根具備public的getter/setter,這自己就是技術實現上的一種約束,好比某些ORM工具會使用reflection,經過讀寫對象的property來改變對象狀態。爲何ORM工具要選擇properties,而不是fields?由於這些框架不但願本身的介入會改變對象對其狀態的封裝級別(也就是訪問限制)。在引入CQRS後,ORM已經沒有太多的用武之地了,固然從技術選型的角度看,你仍然能夠選擇ORM,但就像關係型數據庫那樣,它已經顯得沒那麼重要了。
事件溯源(Event Sourcing)
在某些狀況下,咱們不只須要知道對象的當前狀態是什麼,並且還須要知道,對象經歷了哪些路程,纔得到了當前這樣的狀態。Martin Fowler在介紹Event Sourcing的時候,舉了個郵包跟蹤(Package Tracking)的例子。在經典的DDD實踐中,咱們只能經過Shipment.Location來得到郵包的當前位置,卻沒辦法得到郵包經歷過哪些地址而最終到達當前的地址。
爲了使咱們的業務系統具備記錄對象歷史狀態的能力,咱們使用事件驅動的領域模型來實現咱們的業務系統。簡而言之,就是對模型對象狀態的修改,僅容許經過事件的途徑實現,外界沒法經過任何其餘途徑修改對象的狀態。那麼,記錄對象的狀態修改歷史,就只須要記錄事件的類型以及發生順序便可,由於對象的狀態是由領域事件更改的。因而,也就能理解上面所講的爲何在Event Sourcing的實現中,領域對象將再也不具備公有屬性,或者說,至少再也不具備公有的setter屬性。
當對象的狀態被修改後,咱們可能但願將對象保存到持久化機制,這一點與經典的DDD實踐上的考慮是相似的。而與之不一樣的是,如今咱們保存的已再也不是某個領域對象在某個時間點上的狀態,而是促使對象將其狀態改變到當前點的一系列事件。由此,倉儲(Repository)的實現須要發生變化,它須要有保存領域事件的功能,同時還須要有經過一系列保存的事件數據,重建聚合根的能力。看到這裏,你就知道爲何會有Event Sourcing這個概念了:所謂Event Sourcing,就是「經過事件追溯對象狀態的起源(與通過)」,它容許你經過記錄下來的事件,將你的領域模型恢復到以前任意一個時間點。你可能會興奮地說:個人領域模型開始支持事件回放與模型重建了!
Event Sourcing讓咱們「透過現象看本質」,使咱們更進一步地瞭解到「對象持久化」的具體含義,其實也就是對象狀態的持久化。只不過,Event Sourcing並非直接保存了對象的狀態,而是一系列引發狀態變化的領域事件。
仍然以上面的更改客戶姓名爲例,在引入領域事件與Event Sourcing以後,整個模型的結構發生了變化,如下是相關代碼,僅供參考。
[Serializable] public partial class CustomerCreatedEvent : DomainEvent { public string UserName { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime DayOfBirth { get; set; } } [Serializable] public partial class ChangeNameEvent : DomainEvent { public string FirstName{get;set;} public string LastName{get;set;} } public partial class Customer : SourcedAggregationRoot { private DateTime dayOfBirth; private string userName; private string password; private string firstName; private string lastName; public Customer(string userName, string password, string firstName, string lastName, DateTime dayOfBirth) { this.RaiseEvent<CustomerCreatedEvent>(new CustomerCreatedEvent { DayOfBirth = dayOfBirth, FirstName = firstName, LastName = lastName, UserName = userName, Password = password }); } public void ChangeName(string firstName, string lastName) { this.RaiseEvent<ChangeNameEvent>(new ChangeNameEvent { FirstName = firstName, LastName = lastName }); } // Handles the ChangeNameEvent by using Reflection [Handles(typeof(ChangeNameEvent))] private void DoChangeName(ChangeNameEvent e) { this.firstName = e.FirstName; this.lastName = e.LastName; } // Handles the CustomerCreatedEvent by using Reflection [Handles(typeof(CustomerCreatedEvent))] private void DoCreateCustomer(CustomerCreatedEvent e) { this.firstName = e.FirstName; this.lastName = e.LastName; this.userName = e.UserName; this.password = e.Password; this.dayOfBirth = e.DayOfBirth; } } |
上面的代碼中定義了兩個Domain Event:CustomerCreatedEvent和ChangeNameEvent。在Customer聚合根的構造函數中,直接觸發CustomerCreatedEvent以便該事件的訂閱者對Customer對象進行初始化;而在Customer聚合根的ChangeName方法中,則直接觸發ChangeNameEvent以便該事件的訂閱者對Customer的first name和last name做修改。Customer的基類SourcedAggregationRoot則在領域事件被觸發的時候經過Reflection機制得到內部的事件處理函數,並調用該函數完成事件處理。在上面的例子中,也就是DoChangeName和DoCreateCustomer這兩個方法。在這裏須要注意的是,相似DoChangeName和DoCreateCustomer這樣的事件處理函數中,僅容許包含對對象狀態的設置邏輯。由於若是引入其它操做的話,很難保證經過這些操做,對象的狀態不會發生改變。
深刻思考上面的設計會發現一個問題,也就是當模型對象變得很是龐大,或者隨着時間的推移,領域事件將變得愈來愈多,因而經過Event Sourcing來重建聚合根的過程也會變得愈來愈耗時,由於每一次從建都須要從最先發生的事件開始。爲了解決這個問題,Event Sourcing引入了「快照(Snapshots)」。
快照(Snapshots)
Snapshot的設計其實很簡單。標準的CQRS實現中,採用「每產生N個領域事件,則對對象作一次Snapshot」的簡單規則。設計人員其實能夠根據本身的實際狀況定義N的取值,甚至能夠選用特定的Snapshot規則,以提升對象重建的效率。當須要經過倉儲得到某一個聚合根實體時,倉儲會首先從Snapshot Store中得到最近一次的快照,而後再在由此快照還原的聚合根實體上逐個應用快照以後所產生的領域事件,由此大大加速了對象重建的過程。快照一般採用GoF Memento模式實現。請注意:CQRS引入快照的概念僅僅是爲了解決對象重建的效率問題,它並不能替代領域事件所能表述的含義。換句話說,即便引入快照,也不能表示咱們可以將快照以前的全部事件從事件存儲(Event Store)中刪除。由於,咱們記錄領域事件的目的,是爲了Event Sourcing,而不是Snapshots。
事件存儲(Event Store)
一般,事件存儲是一個關係型數據庫,用來保存引發領域對象狀態更改的全部領域事件。如上所述,在CQRS結構的系統實現中,數據庫已經再也不直接保存對象的當前狀態了,保存的只是引發對象狀態發生變化的領域事件。因而,數據庫的數據結構很是單一,就是單純的領域事件數據。事件數據的寫入、讀取都變得很是簡單高速,根本無需ORM的介入,直接使用SQL或者存儲過程操做事件存儲便可,既簡單又高效。讀到這裏,你會發現,雖然系統是用的一個稱之爲Event Store的機制保存了領域事件,但這個Event Store已經成爲了整個系統數據存儲的核心。更進一步考慮,Event Store中的事件數據是在倉儲執行「保存」操做時,從領域模型中收集並寫入的,也就意味着,最新的、最真實的數據仍然存在於領域模型中,正好符合DDD面向領域的思想,同時也引出了另外一深層次的考慮:In Memory Domain!
回到結構
在完成對「對象狀態」、「事件溯源(Event Sourcing)」、「快照(Snapshots)」以及「事件存儲(Event Store)」的討論後,咱們再來看整個CQRS的結構,這樣就顯得更加清楚。上文【CQRS體系結構模式】圖中,用戶操做被分爲命令部分(圖中上半部分)和查詢部分(圖中下半部分)。
總結
本文介紹了CQRS模式的基本結構,並對其中一些重要概念做了註釋,也是我在實踐和思考當中總結出來的內容(PS:轉載請註明出處)。學習過DDD而剛剛開始CQRS的朋友,在閱讀一些資料的時候勢必會感到疑惑,但願本文可以幫助到這些朋友。好比最開始閱讀的時候,我也不知道爲何必定要經過領域事件去更改對象狀態,而不是在對象狀態變動的時候,去觸發領域事件,由於當時我仍然但願可以在Domain Model中方便地使用getter/setter,我當時也但願可以讓Domain Model同時適應於經典DDD和CQRS架構。在通過屢次嘗試後發現,這種作法是不合理、不可取的,也正如Udi Dahan所說:CQRS是一種模式,既然是模式,就是用來解決特定問題的。
仍是一句老話:視需求而定。不要由於CQRS因此CQRS。雖然能夠很大程度地提高系統性能,雖然能夠使系統具備auditing的能力,雖然能夠實現Domain-Centralized,雖然可讓數據存儲變得更加簡單,雖然給咱們提供了不少技術選型的機會,可是,CQRS也有不少不足點,好比結構實現較繁雜,數據同步穩定性難以獲得保證,事件溯源(Event Sourcing)出錯時,模型對象狀態的恢復等等。仍是引用Udi Dahan的一句話:簡單,但不容易!
是時候總結一下本系列文章了。仍是應該自我批評一下,因爲我的瑣事多,加上工做繁忙,整個系列文章弄了大半年才斷斷續續寫完。在撰寫文章的過程當中,也獲得了你們的理解與支持,並讓更多的朋友開始關注領域驅動設計,非常感激!在接下來的其它博文中,我將繼續討論領域驅動設計的實踐經驗。
本系列文章首先從領域驅動設計的基礎思想出發,討論了基於.NET EntityFramework的領域驅動設計經驗,這包括對實體、值對象、工廠和倉儲實現方式的討論、對EntityFramework所提供的開發工具功能點的討論,還包括了規約模式及其.NET實現。從討論中咱們能夠了解到,目前Microsoft .NET EntityFramework在對領域驅動設計的支持上仍是有一些不足的地方,好比對值對象的支持、對實體行爲以及事件驅動的支持,但同時咱們也看到了.NET在DDD上取得的巨大進步,這包括:具備DSL(Domain Specific Language)特質的語言集成查詢(LINQ)、面向實體的領域對象模型(LINQ to SQL是面向數據的)、複合數據類型支持。畢竟.NET EntityFramework是一種產品,它須要考慮廣大用戶的需求,因此也會包括一些Anti-DDD的元素在裏面。
在討論了經典DDD的EntityFramework實踐經驗以後,本系列文章還引出了兩個擴展話題:服務與CQRS體系結構模式。CQRS體系結構模式是近年來DDD社區總結出來的一種新的DDD實踐模式,也是目前DDD社區中討論的較多的一種體系結構模式。它以CQS思想爲基礎,以事件溯源(Event Sourcing)爲核心,將應用系統的命令與查詢實現職責分離,從而得到Event Auditing的功能,同時大大提升了系統的運行效率,併爲架構技術選型和團隊資源配置(Resource Configuration)帶來了廣闊空間。有關CQRS的設計與實現,我會在後續的文章中繼續介紹。