EntityFramework之領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)踐【擴(kuò)展閱讀】:CQRS體系結(jié)構(gòu)模式
CQRS體系結(jié)構(gòu)模式
本文將對(duì)CQRS(Command Query Responsibility Segregation,命令查詢職責(zé)分離)模式做一個(gè)相對(duì)全面的介紹??梢赃@么說(shuō),CQRS打破了經(jīng)典的領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)踐,在應(yīng)用CQRS的整個(gè)過(guò)程中,你將會(huì)以另一種不同的角度去考慮問(wèn)題并尋求解決方案。比如,CQRS是事件驅(qū)動(dòng)的體系結(jié)構(gòu),事件是如何產(chǎn)生如何分發(fā)又是如何處理的?事件驅(qū)動(dòng)的體系結(jié)構(gòu)適用于哪些類(lèi)型的應(yīng)用系統(tǒng)?CQRS中的倉(cāng)儲(chǔ),與經(jīng)典DDD中的倉(cāng)儲(chǔ)又有何異同?等等這些問(wèn)題,都給我們留下了無(wú)限的思考空間。
背景
在講CQRS之前,我們先了解一下CQS(Command-Query Separation,命令查詢)模式。名字上看,兩者沒(méi)什么差別,然而CQRS應(yīng)該說(shuō)是,在DDD的實(shí)踐中引入CQS理論而出現(xiàn)的一種體系結(jié)構(gòu)模式。CQS模式最早由著名軟件大師Bertrand Meyer(Eiffel語(yǔ)言之父,面向?qū)ο箝_(kāi)-閉原則OCP提出者)提出,他認(rèn)為,對(duì)象的行為僅有兩種:命令和查詢,不存在第三種情況。用他自己的話來(lái)說(shuō),就是:“提問(wèn)永遠(yuǎn)無(wú)法改變答案”。根據(jù)CQS,任何方法都可以拆分為命令和查詢兩個(gè)部分。比如,下面的代碼:
-
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;
-
}
當(dāng)命令和查詢被分離的時(shí)候,我們將會(huì)有更多的機(jī)會(huì)去把握整個(gè)事情的細(xì)節(jié)。比如我們可以對(duì)系統(tǒng)的“命令”部分和“查詢”部分分別采用不同的技術(shù)架構(gòu),以使得系統(tǒng)具有更好的擴(kuò)展性,并獲得更好的性能。在DDD領(lǐng)域中,Greg Young和Eric Evans根據(jù)Bertrand Meyer的CQS模式,結(jié)合實(shí)際項(xiàng)目經(jīng)驗(yàn),總結(jié)了CQRS體系結(jié)構(gòu)模式。
結(jié)構(gòu)
整個(gè)系統(tǒng)結(jié)構(gòu)被分為兩個(gè)部分:命令部分和查詢部分。我根據(jù)自己的體會(huì),描繪了CQRS的體系結(jié)構(gòu)簡(jiǎn)圖如下,供大家參考。在討論CQRS體系結(jié)構(gòu)之前,我們有必要事先弄清楚這樣幾個(gè)概念:對(duì)象狀態(tài)、事件溯源(Event Sourcing)、快照(Snapshots)以及事件存儲(chǔ)(Event Store)。討論的過(guò)程中你會(huì)發(fā)現(xiàn),很多概念與我們之前對(duì)經(jīng)典DDD的理解相比,有著很大的不同。
對(duì)象狀態(tài)
這是一個(gè)大家耳熟能詳?shù)母拍盍恕J裁词菍?duì)象狀態(tài)?在被面向?qū)ο缶幊蹋∣OP)“熏陶”了很久的朋友,一聽(tīng)到“對(duì)象狀態(tài)”,馬上想到了一對(duì)對(duì)的getter/setter屬性。尤其是.NET程序員,在C# 3.0及以后版本中,引入了Auto-Property的概念,于是,對(duì)象的屬性就很容易地成為了對(duì)象狀態(tài)的代名詞。在這里,我們應(yīng)該看到問(wèn)題的本質(zhì),即使是Auto-Property,它也無(wú)非是對(duì)對(duì)象字段的一種封裝,只不過(guò)在使用Auto-Property的時(shí)候,C#編譯器會(huì)在后臺(tái)創(chuàng)建一個(gè)私有的、匿名的字段(field),而Property則成為了從外部訪問(wèn)該字段的唯一途徑。換句話說(shuō),對(duì)象的狀態(tài)是保存在這些字段里的,對(duì)象屬性無(wú)非是訪問(wèn)字段的facade。在這里澄清這樣一個(gè)事實(shí),就是為了當(dāng)你繼續(xù)閱讀本文的時(shí)候,不至于對(duì)事件溯源(Event Sourcing)的某些具體實(shí)現(xiàn)感到困惑。在Event Sourcing的具體實(shí)現(xiàn)中,領(lǐng)域?qū)ο蟛辉傩枰邆涔械膶傩?,至少外界無(wú)法通過(guò)公有屬性改變對(duì)象狀態(tài)(即setter被定義為private,甚至沒(méi)有setter)。這與經(jīng)典的DDD設(shè)計(jì)相比,無(wú)疑是一個(gè)重大改變。例如,現(xiàn)在我要改變某個(gè)Customer的狀態(tài),如果采用經(jīng)典DDD的實(shí)現(xiàn)方式,就是:
-
[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();
-
}
-
}
現(xiàn)在,很多ORM工具都需要聚合根具有public的getter/setter,這本身就是技術(shù)實(shí)現(xiàn)上的一種約束,比如某些ORM工具會(huì)使用reflection,通過(guò)讀寫(xiě)對(duì)象的property來(lái)改變對(duì)象狀態(tài)。為什么ORM工具要選擇properties,而不是fields?因?yàn)檫@些框架不希望自己的介入會(huì)改變對(duì)象對(duì)其狀態(tài)的封裝級(jí)別(也就是訪問(wèn)限制)。在引入CQRS后,ORM已經(jīng)沒(méi)有太多的用武之地了,當(dāng)然從技術(shù)選型的角度看,你仍然可以選擇ORM,但就像關(guān)系型數(shù)據(jù)庫(kù)那樣,它已經(jīng)顯得沒(méi)那么重要了。
事件溯源(Event Sourcing)
在某些情況下,我們不僅需要知道對(duì)象的當(dāng)前狀態(tài)是什么,而且還需要知道,對(duì)象經(jīng)歷了哪些路程,才獲得了當(dāng)前這樣的狀態(tài)。Martin Fowler在介紹Event Sourcing的時(shí)候,舉了個(gè)郵包跟蹤(Package Tracking)的例子。在經(jīng)典的DDD實(shí)踐中,我們只能通過(guò)Shipment.Location來(lái)獲得郵包的當(dāng)前位置,卻沒(méi)辦法獲得郵包經(jīng)歷過(guò)哪些地址而最終到達(dá)當(dāng)前的地址。
為了使我們的業(yè)務(wù)系統(tǒng)具有記錄對(duì)象歷史狀態(tài)的能力,我們使用事件驅(qū)動(dòng)的領(lǐng)域模型來(lái)實(shí)現(xiàn)我們的業(yè)務(wù)系統(tǒng)。簡(jiǎn)而言之,就是對(duì)模型對(duì)象狀態(tài)的修改,僅允許通過(guò)事件的途徑實(shí)現(xiàn),外界無(wú)法通過(guò)任何其他途徑修改對(duì)象的狀態(tài)。那么,記錄對(duì)象的狀態(tài)修改歷史,就只需要記錄事件的類(lèi)型以及發(fā)生順序即可,因?yàn)閷?duì)象的狀態(tài)是由領(lǐng)域事件更改的。于是,也就能理解上面所講的為什么在Event Sourcing的實(shí)現(xiàn)中,領(lǐng)域?qū)ο髮⒉辉倬哂泄袑傩?,或者說(shuō),至少不再具有公有的setter屬性。
當(dāng)對(duì)象的狀態(tài)被修改后,我們可能希望將對(duì)象保存到持久化機(jī)制,這一點(diǎn)與經(jīng)典的DDD實(shí)踐上的考慮是類(lèi)似的。而與之不同的是,現(xiàn)在我們保存的已不再是某個(gè)領(lǐng)域?qū)ο笤谀硞€(gè)時(shí)間點(diǎn)上的狀態(tài),而是促使對(duì)象將其狀態(tài)改變到當(dāng)前點(diǎn)的一系列事件。由此,倉(cāng)儲(chǔ)(Repository)的實(shí)現(xiàn)需要發(fā)生變化,它需要有保存領(lǐng)域事件的功能,同時(shí)還需要有通過(guò)一系列保存的事件數(shù)據(jù),重建聚合根的能力??吹竭@里,你就知道為什么會(huì)有Event Sourcing這個(gè)概念了:所謂Event Sourcing,就是“通過(guò)事件追溯對(duì)象狀態(tài)的起源(與經(jīng)過(guò))”,它允許你通過(guò)記錄下來(lái)的事件,將你的領(lǐng)域模型恢復(fù)到之前任意一個(gè)時(shí)間點(diǎn)。你可能會(huì)興奮地說(shuō):我的領(lǐng)域模型開(kāi)始支持事件回放與模型重建了!
Event Sourcing讓我們“透過(guò)現(xiàn)象看本質(zhì)”,使我們更進(jìn)一步地了解到“對(duì)象持久化”的具體含義,其實(shí)也就是對(duì)象狀態(tài)的持久化。只不過(guò),Event Sourcing并不是直接保存了對(duì)象的狀態(tài),而是一系列引起狀態(tài)變化的領(lǐng)域事件。
仍然以上面的更改客戶姓名為例,在引入領(lǐng)域事件與Event Sourcing之后,整個(gè)模型的結(jié)構(gòu)發(fā)生了變化,以下是相關(guān)代碼,僅供參考。
-
[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;
-
}
-
}
上面的代碼中定義了兩個(gè)Domain Event:CustomerCreatedEvent和ChangeNameEvent。在Customer聚合根的構(gòu)造函數(shù)中,直接觸發(fā)CustomerCreatedEvent以便該事件的訂閱者對(duì)Customer對(duì)象進(jìn)行初始化;而在Customer聚合根的ChangeName方法中,則直接觸發(fā)ChangeNameEvent以便該事件的訂閱者對(duì)Customer的first name和last name作修改。Customer的基類(lèi)SourcedAggregationRoot則在領(lǐng)域事件被觸發(fā)的時(shí)候通過(guò)Reflection機(jī)制獲得內(nèi)部的事件處理函數(shù),并調(diào)用該函數(shù)完成事件處理。在上面的例子中,也就是DoChangeName和DoCreateCustomer這兩個(gè)方法。在這里需要注意的是,類(lèi)似DoChangeName和DoCreateCustomer這樣的事件處理函數(shù)中,僅允許包含對(duì)對(duì)象狀態(tài)的設(shè)置邏輯。因?yàn)槿绻肫渌僮鞯脑挘茈y保證通過(guò)這些操作,對(duì)象的狀態(tài)不會(huì)發(fā)生改變。
深入思考上面的設(shè)計(jì)會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,也就是當(dāng)模型對(duì)象變得非常龐大,或者隨著時(shí)間的推移,領(lǐng)域事件將變得越來(lái)越多,于是通過(guò)Event Sourcing來(lái)重建聚合根的過(guò)程也會(huì)變得越來(lái)越耗時(shí),因?yàn)槊恳淮螐慕ǘ夹枰獜淖钤绨l(fā)生的事件開(kāi)始。為了解決這個(gè)問(wèn)題,Event Sourcing引入了“快照(Snapshots)”。
快照(Snapshots)
Snapshot的設(shè)計(jì)其實(shí)很簡(jiǎn)單。標(biāo)準(zhǔn)的CQRS實(shí)現(xiàn)中,采用“每產(chǎn)生N個(gè)領(lǐng)域事件,則對(duì)對(duì)象做一次Snapshot”的簡(jiǎn)單規(guī)則。設(shè)計(jì)人員其實(shí)可以根據(jù)自己的實(shí)際情況定義N的取值,甚至可以選用特定的Snapshot規(guī)則,以提高對(duì)象重建的效率。當(dāng)需要通過(guò)倉(cāng)儲(chǔ)獲得某一個(gè)聚合根實(shí)體時(shí),倉(cāng)儲(chǔ)會(huì)首先從Snapshot Store中獲得最近一次的快照,然后再在由此快照還原的聚合根實(shí)體上逐個(gè)應(yīng)用快照之后所產(chǎn)生的領(lǐng)域事件,由此大大加速了對(duì)象重建的過(guò)程??煺胀ǔ2捎肎oF Memento模式實(shí)現(xiàn)。請(qǐng)注意:CQRS引入快照的概念僅僅是為了解決對(duì)象重建的效率問(wèn)題,它并不能替代領(lǐng)域事件所能表述的含義。換句話說(shuō),即使引入快照,也不能表示我們能夠?qū)⒖煺罩暗乃惺录氖录鎯?chǔ)(Event Store)中刪除。因?yàn)椋覀冇涗涱I(lǐng)域事件的目的,是為了Event Sourcing,而不是Snapshots。
事件存儲(chǔ)(Event Store)
通常,事件存儲(chǔ)是一個(gè)關(guān)系型數(shù)據(jù)庫(kù),用來(lái)保存引起領(lǐng)域?qū)ο鬆顟B(tài)更改的所有領(lǐng)域事件。如上所述,在CQRS結(jié)構(gòu)的系統(tǒng)實(shí)現(xiàn)中,數(shù)據(jù)庫(kù)已經(jīng)不再直接保存對(duì)象的當(dāng)前狀態(tài)了,保存的只是引起對(duì)象狀態(tài)發(fā)生變化的領(lǐng)域事件。于是,數(shù)據(jù)庫(kù)的數(shù)據(jù)結(jié)構(gòu)非常單一,就是單純的領(lǐng)域事件數(shù)據(jù)。事件數(shù)據(jù)的寫(xiě)入、讀取都變得非常簡(jiǎn)單高速,根本無(wú)需ORM的介入,直接使用SQL或者存儲(chǔ)過(guò)程操作事件存儲(chǔ)即可,既簡(jiǎn)單又高效。讀到這里,你會(huì)發(fā)現(xiàn),雖然系統(tǒng)是用的一個(gè)稱(chēng)之為Event Store的機(jī)制保存了領(lǐng)域事件,但這個(gè)Event Store已經(jīng)成為了整個(gè)系統(tǒng)數(shù)據(jù)存儲(chǔ)的核心。更進(jìn)一步考慮,Event Store中的事件數(shù)據(jù)是在倉(cāng)儲(chǔ)執(zhí)行“保存”操作時(shí),從領(lǐng)域模型中收集并寫(xiě)入的,也就意味著,最新的、最真實(shí)的數(shù)據(jù)仍然存在于領(lǐng)域模型中,正好符合DDD面向領(lǐng)域的思想,同時(shí)也引出了另一深層次的考慮:In Memory Domain!
回到結(jié)構(gòu)
在完成對(duì)“對(duì)象狀態(tài)”、“事件溯源(Event Sourcing)”、“快照(Snapshots)”以及“事件存儲(chǔ)(Event Store)”的討論后,我們?cè)賮?lái)看整個(gè)CQRS的結(jié)構(gòu),這樣就顯得更加清楚。上文【CQRS體系結(jié)構(gòu)模式】圖中,用戶操作被分為命令部分(圖中上半部分)和查詢部分(圖中下半部分)。
- 用戶與領(lǐng)域?qū)拥慕换?,是以命令的方式進(jìn)行的:用戶通過(guò)Command Service向領(lǐng)域模型發(fā)送命令。Command Service通常被實(shí)現(xiàn)為.NET WCF Service。Command Bus在接收到命令后,將命令指派到命令執(zhí)行器由其負(fù)責(zé)執(zhí)行(可以參考GoF Command模式。TBD: 可以選擇更符合CQRS實(shí)現(xiàn)的其它途徑)。命令執(zhí)行器在執(zhí)行命令時(shí),通過(guò)領(lǐng)域事件更改對(duì)象狀態(tài),并通過(guò)倉(cāng)儲(chǔ)保存領(lǐng)域?qū)ο?。而倉(cāng)儲(chǔ)并非直接將對(duì)象狀態(tài)保存到外部持久化機(jī)制,而僅僅是從領(lǐng)域?qū)ο笾蝎@得已產(chǎn)生的一系列領(lǐng)域事件,并將這些事件保存到Event Store,同時(shí)將事件發(fā)布到事件總線Event Bus
- Event Handler可以訂閱Event Bus中的事件,并在事件發(fā)生時(shí)作相關(guān)處理。上文在討論服務(wù)的時(shí)候,有個(gè)例子就是利用基礎(chǔ)結(jié)構(gòu)層服務(wù)發(fā)送SMS消息,在CQRS的體系結(jié)構(gòu)中,我們完全可以在此訂閱Warehouse Transferred事件,并調(diào)用基礎(chǔ)結(jié)構(gòu)層服務(wù)發(fā)送SMS消息。Domain Model完全不知道自己的內(nèi)部事件被觸發(fā)后,會(huì)出現(xiàn)什么情況,而Event Handler則會(huì)處理這些情況(Domain Model與基礎(chǔ)結(jié)構(gòu)層完全解耦)
- 在Event Handler中,有一種特殊的Event Handler,稱(chēng)之為Synchronizer或者Denormalizer,其作用就是為了同步“Query Database”。Query Database是為查詢提供數(shù)據(jù)源的存儲(chǔ)機(jī)制,用戶在UI上看到的查詢數(shù)據(jù)均來(lái)源于此數(shù)據(jù)庫(kù)。因此,CQRS不僅分離了用戶操作,而且分離了數(shù)據(jù)源,這樣做的一個(gè)最大的優(yōu)點(diǎn)就是,設(shè)計(jì)人員可以根據(jù)UI的需求來(lái)配置和優(yōu)化Query Database,例如,可以將Query Database設(shè)計(jì)為一張數(shù)據(jù)表對(duì)應(yīng)一個(gè)UI界面,于是,用戶查詢變得非常靈活高效。這里也可以使用DDD中的Repository結(jié)合ORM實(shí)現(xiàn)數(shù)據(jù)讀取,與處于Domain Layer中的Repository不同,這個(gè)Repository就是DDD中所提到的經(jīng)典型倉(cāng)儲(chǔ)了,你可以靈活地使用規(guī)約模式。當(dāng)然,你也可以不使用ORM而直接SQL甚至No SQL,一切取決于用戶需求與技術(shù)選型。我們還可以根據(jù)需要,對(duì)Synchronizer和Denormalizer的實(shí)現(xiàn)采用緩存,比如,對(duì)于無(wú)需實(shí)時(shí)更新的內(nèi)容,可以每捕獲N個(gè)事件同步一次Query Database,或者當(dāng)有客戶端query請(qǐng)求時(shí),再做一次同步,這也是提高效率的一種有效方法
- 用戶UI通過(guò)Data Proxy獲得查詢結(jié)果數(shù)據(jù),WCF將數(shù)據(jù)以DTO的形式發(fā)送給客戶端
總結(jié)
本文介紹了CQRS模式的基本結(jié)構(gòu),并對(duì)其中一些重要概念作了注釋?zhuān)彩俏以趯?shí)踐和思考當(dāng)中總結(jié)出來(lái)的內(nèi)容(PS:轉(zhuǎn)載請(qǐng)注明出處)。學(xué)習(xí)過(guò)DDD而剛剛開(kāi)始CQRS的朋友,在閱讀一些資料的時(shí)候勢(shì)必會(huì)感到疑惑,希望本文能夠幫助到這些朋友。比如最開(kāi)始閱讀的時(shí)候,我也不知道為什么一定要通過(guò)領(lǐng)域事件去更改對(duì)象狀態(tài),而不是在對(duì)象狀態(tài)變更的時(shí)候,去觸發(fā)領(lǐng)域事件,因?yàn)楫?dāng)時(shí)我仍然希望能夠在Domain Model中方便地使用getter/setter,我當(dāng)時(shí)也希望能夠讓Domain Model同時(shí)適應(yīng)于經(jīng)典DDD和CQRS架構(gòu)。在經(jīng)過(guò)多次嘗試后發(fā)現(xiàn),這種做法是不合理、不可取的,也正如Udi Dahan所說(shuō):CQRS是一種模式,既然是模式,就是用來(lái)解決特定問(wèn)題的。
還是一句老話:視需求而定。不要因?yàn)镃QRS所以CQRS。雖然可以很大程度地提升系統(tǒng)性能,雖然可以使系統(tǒng)具有auditing的能力,雖然可以實(shí)現(xiàn)Domain-Centralized,雖然可以讓數(shù)據(jù)存儲(chǔ)變得更加簡(jiǎn)單,雖然給我們提供了很多技術(shù)選型的機(jī)會(huì),但是,CQRS也有很多不足點(diǎn),比如結(jié)構(gòu)實(shí)現(xiàn)較繁雜,數(shù)據(jù)同步穩(wěn)定性難以得到保證,事件溯源(Event Sourcing)出錯(cuò)時(shí),模型對(duì)象狀態(tài)的恢復(fù)等等。還是引用Udi Dahan的一句話:簡(jiǎn)單,但不容易!

浙公網(wǎng)安備 33010602011771號(hào)