開源地址:https://github.com/tangxuehua/enode
上一篇文章,我給大家分享了我的一個基於DDD以及EDA架構的框架enode,但是只是介紹了一個大概。接下來我准備用很多一篇篇詳細但不冗長的文章介紹每個點。盡量爭取一次不介紹太多內容,但希望每次介紹完后都能讓大家知道這個小點的設計思想,以及為了解決的問題。
好了,這篇文章,我主要想介紹的是EDA思想在enode框架中如何體現?
經典DDD的基於領域服務的實現方式
一般的應用程序,如果一個用戶動作會涉及多個聚合根的修改,我們通常會在應用層服務中創建一個unit of work,然后,我們可能會設計一個領域服務類,在該領域服務類里,修改多個聚合根,然后應用層服務將整個unit of work中的修改一次性以事務的方式提交到數據庫。這種方式就是以事務的方式來實現涉及多個聚合根修改的強一致性。以銀行轉賬這個經典的場景作為分析案例:
public interface IBankAccountService { void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount); } public class BankAccountService : IBankAccountService { private IContextManager _contextManager; private TransferMoneyService _transferMoneyService; public BankAccountService(IContextManager contextManager, TransferMoneyService transferMoneyService) { _contextManager = contextManager; _transferMoneyService = transferMoneyService; } public void TransferMoney(Guid sourceBankAccountId, Guid targetBankAccountId, double amount) { using (var context = _contextManager.GetContext()) { var sourceAccount = context.Load<BankAccount>(sourceBankAccountId); var targetAccount = context.Load<BankAccount>(targetBankAccountId); _transferMoneyService.TransferMoney(sourceAccount, targetAccount, amount); context.SaveChanges(); } } }
一次銀行轉賬,最核心的動作就是源賬號轉出錢,目標賬號轉入錢;當然實際的銀行轉賬肯定不是這么簡單,也肯定不是這么實現。我拿這個作為例子只是為了通過這個大家都熟知的簡單例子來分析如果一個用戶場景涉及不止一個聚合根的修改的時候,如果基於經典的DDD的方式,我們是如何實現的。如上面的代碼所示,我們可能會設計一個應用層服務,如上面的IBankAccountService,該應用層服務里有一個TransferMoney的方法,表示用於實現銀行轉賬的功能;然后該應用層服務會進一步調用一個領域層的轉賬領域服務,就是上面代碼中的TransferMoneyService,按照Eric Evans所說,領域服務應該是一個以動詞命名的服務,一個領域服務可以明確對應到領域中的一個有業務含義的領域動作,此例就是“轉賬”,所以我設計了一個TransferMoneyService的以動詞來命名的領域服務,該服務的TransferMoney方法實現了銀行轉賬的核心業務邏輯。
上面這個例子中,按照經典DDD,我們應該在應用層實現流程控制邏輯以及事務等東西;所以大家可以看到,以上代碼中,我們是先獲取一個unit of work,即上面代碼中的context,最后調用context.SaveChanges方法,該方法的職責就是將當前上下文的所有修改以事務的方式提交到數據庫。好了,上面這個例子我們分析了經典DDD關於如何實現一個會涉及多個聚合根新建或修改的用戶場景;
enode的事件驅動的實現方式
我一直說enode是一個基於事件驅動架構(EDA,Event-Driven Architecture)的框架。且深藍醫生在前面的回復中也對什么是事件驅動的架構有疑惑。所以我想說一下我對事件驅動架構的理解。
EDA,顧名思義,我覺得就是事件驅動的,那事件到底驅動了什么呢?我覺得就是事件驅動狀態的修改。如何理解呢?就是說,假如你要修改一個對象的狀態,那就不是直接調用該對象的某個方法來修改它或者直接通過修改某個對象的屬性來達到修改該對象狀態的目的;取而代之的是,我們需要先觸發一個事件,然后該對象會響應該事件,然后在響應函數中修改對象自己的狀態。當然,更廣義和權威的事件驅動架構的定義和解釋,我覺得很容易找啊,比如直接去百度上搜一下或直接到wikipedia上搜一下,也很容易就能找到標准的解釋。比如這里就是我找到的解釋。其實,更大范圍的解釋,就是一種publish-subscriber模式,就是有一個事件生產者產生事件,然后有一個類似event publisher的東西會把這個事件廣播出去,然后所有的事件消費者就能消費該事件了。通過這樣的pub-sub,我們的應用程序的各個組件之間可以做到很徹底的解耦,並且可以做到更靈活的擴展性。這兩點的好處應該是很容易體會到的。比如更徹底的解耦是,比如本來一個對象要和另一個對象交互,那它可能要引用該對象,然后調用該對象的某個方法,從而實現對象之間的交互。這種實現方式會讓兩個對象綁定在一起,比如a對象調用b對象的方法,那意味着a需要依賴b對象;而通過事件驅動的方式,a對象只要publish一個事件,然后b對象響應該事件即可,這樣a對象就不知道b對象的存在了,也就是a對象不在依賴b對象;擴展性,就是本來一個事件,可能只有1個事件響應者,但是后面可能由於功能擴展等原因,我們需要增加一個事件響應者,這樣就能方便的做到在不改變原來任何代碼的基礎之上,增加新功能了;其他的好處就不多分析了,有興趣的可以再去看看資料吧。
上面這一段,我簡單介紹了我所理解的EDA,以及它的基本的好處。下面我們看看,在enode中,我們是如何利用EDA這種原理的。為了簡化,我先用一個簡單的例子說明一下,就用我源代碼中的NoteSample吧,反正也能一樣說明事件驅動的影子在哪里。看以下的代碼:
[Serializable] public class Note : AggregateRoot<Guid>, IEventHandler<NoteCreated>, //訂閱事件 IEventHandler<NoteTitleChanged> { public string Title { get; private set; } public DateTime CreatedTime { get; private set; } public DateTime UpdatedTime { get; private set; } public Note() : base() { } public Note(Guid id, string title) : base(id) { var currentTime = DateTime.Now; //觸發事件 RaiseEvent(new NoteCreated(Id, title, currentTime, currentTime)); } public void ChangeTitle(string title) { //觸發事件 RaiseEvent(new NoteTitleChanged(Id, title, DateTime.Now)); } //事件響應函數 void IEventHandler<NoteCreated>.Handle(NoteCreated evnt) { //在響應函數中修改自己的狀態,這里可以體現出EDA的影子,就是事件驅動狀態的修改 Title = evnt.Title; CreatedTime = evnt.CreatedTime; UpdatedTime = evnt.UpdatedTime; } //事件響應函數 void IEventHandler<NoteTitleChanged>.Handle(NoteTitleChanged evnt) { //同上解釋 Title = evnt.Title; UpdatedTime = evnt.UpdatedTime; } }
上面的例子中,Note是一個聚合根,它會響應兩個事件:NoteCreated, NoteTitleChanged。要實現事件響應,我們可以通過實現框架提供的IEventHandler<T>接口,就能告訴框架,我要訂閱什么事件了。
上面代碼中,應該比較詳細的注釋了每段代碼的含義了,應該都能看懂吧。上面這個例子說明了,聚合跟自己的狀態不是在public方法中直接改的,而是基於事件驅動的方式來修改的,所以,大家可以看到,聚合根狀態的修改是在一個內部響應函數中修改的。下面我們再來看一下外部其他對象,如何響應該事件:
//這是一個事件訂閱者,它也響應了Note的兩個事件 public class NoteEventHandler : IEventHandler<NoteCreated>, IEventHandler<NoteTitleChanged> { public void Handle(NoteCreated evnt) { //這里為了簡單,所以只是輸出了一串文字,實際我們可以在這里做任何你想做的事情; Console.WriteLine(string.Format("Note created, title:{0}", evnt.Title)); } public void Handle(NoteTitleChanged evnt) { Console.WriteLine(string.Format("Note title changed, title:{0}", evnt.Title)); } }
通過上面兩個簡單的例子,不知道有沒有解釋清楚,在enode框架中,如何體現EDA?
總結:
我之所以比較喜歡事件驅動這種思想是基於以下理由:
- 就是上面我說的解耦+可擴展;
- 事件可以並行執行;就是說,一個系統中,同時可以有很多事件在並行的產生、傳遞、響應;這樣說,大家可能還理解不了這一點的價值。我說一下並發的概念。通常我們所說的一個網站的並發,比如有5000,是指一個網站在1秒內的所有並發請求數,這么多並發請求數是針對系統中所有的聚合根的;也就是如果平攤到每個聚合根,那並發修改數一般就很低了,比如每秒只有10個並發,甚至只有1個或兩個。這點每個系統有所不同,比如淘寶的商品秒殺活動,那當秒殺開始的時刻,對同一個商品的下單的並發數很高,因為每個商品的每個訂單都意味着要減庫存,所以這個減庫存的並發操作一定很高,實現起來肯定很困難了,不通過可靠的分布式緩存以及樂觀鎖機制,估計很難實現;而比如新浪微博上,我們每個人發微博,雖然整個新浪微博網站的整體並發數很高,因為肯定每秒有非常多的人在寫微博,但是我們同時也知道,大家寫的微博都是獨立的,沒有共享資源,每發表一條微博實際上就是創建一條數據庫記錄而已。所以可以理解為,單個對象無並發;而一般的企業應用或一般的互聯網應用,針對同一資源(同一個聚合根)的並發修改,一般都不高;所以基於這樣的分析和理解,我們知道了,理論上,事件什么時候可以並行產生和執行,什么時候必須排隊。就是:如果兩個事件不是同一個聚合根產生的,那就可以並行處理,事件也可以並行持久化;如果是單個聚合根產生的,那必須按照順序被持久化;所以,根據這樣的理解,我們知道了,一個應用程序,除了單個聚合根上的修改只能串行進行外,其他情況理論上都可以並行執行;這段話說了這么多關於並發數以及事件並行方面的東西,那究竟知道這些有什么用呢?很簡單,只要和傳統的事務模式對比下就知道了,傳統的事務模式,如果要修改多個聚合根,那事務在執行的那一段時間,所有涉及到的聚合根都不能被其他事務所修改;只有等到當前事務執行完成后,其他事務才能執行;而通過事件的方式,由於我們沒有事務的概念,我們唯一要確保的只是一個聚合根上產生的事件必須被一個個按順序持久化,這點我們很簡單,比如我們只要建一個聯合主鍵:聚合根ID+事件版本號,然后做樂觀並發控制即可;所以,事件持久化時,排他的粒度比事務要小,這樣的好處是無阻塞;那么換來的好處就是網站整體的可用性高;但是帶來的壞處是,可能有可能會出現樂觀並發沖突,但這點我們可以通過框架的自動重試功能解決掉;而且,我們也剛分析過,同一個聚合根的並發修改一般是很低的;所以通過事件的方式來達到這種細粒度的對聚合根的修改是非常有意義的。
- 配合Event Sourcing模式,可以讓EDA發揮更大的價值,更准確的說,我們可以讓事件發揮更大的價值;就是:我們不僅可以讓事件作為消息,在系統各個對象或組件甚至是各個系統之間傳遞,還可以用事件來還原整個系統的狀態。這點我會在后面詳細介紹enode框架中如何使用event sourcing這種模式;
