距離發佈上一篇該系列的文章好像已通過了快一個半月了,好吧,我託更了😭。一晃就已經到了3月份,在這櫻花🌸怒放的季節,終於得從新連載該系列了。在停更的期間時不時會收到你們關於DDD的留言和問題,一旦我有時間必定會回覆你們的問題。在此,衷心感謝你們對本系列文章的支持😄。html
在實踐領域驅動設計(DDD)的過程當中,咱們每每會遇到多個領域對象相互交互的狀況。好比聚合根A在執行某操做以前須要獲得聚合根B的某個信號(或某些數據)。若是在單體應用程序中,咱們有條件和機會使得二者進行強引用來完成操做,可是這將直接打破領域驅動設計的規範,從而使得項目不可控,再次回到大泥球的開發。git
如今,我們能夠選取一種更純淨的方式來解決這類問題,而且還可以更清晰的描述領域對象的活動跡象。這就是我們今天的主題 ———— 「領域事件」。那麼到底什麼是領域事件呢?引入領域事件會爲咱們已有的DDD項目帶來哪些益處?是否必定要使用領域事件呢? 本文將從不一樣的角度來帶你們從新認識一下「領域事件」這個概念,而且給出相應的代碼片斷(本教程的代碼片斷都使用的是C#,固然思想是跨越任何編程語言的😀)。github
在原著 《領域驅動設計:軟件核心複雜性應對之道》 其實並無直接說起到關於領域事件的介紹。領域對象是在後期才被做者Evans提出,通過Udi Dahan(Nservicebus做者)和Jimmy Bogard(MetdiaR、AutoMapper做者)等專家後期的不斷實踐和演變纔有了今天的領域事件版本。編程
此處我摘錄了《實現領域驅動設計》書中對領域事件的描述:設計模式
領域專家所關心的發生在領域中的一些事件。
將領域中所發生的活動建模成一系列的離散事件。每一個事件都用領域對象來表示,領域事件是領域模型的組成部分,表示領域中所發生的事情。架構
當您一看到「事件」這個詞語的時候,您可能會一下聯繫到 C# 中的事件,那個基於委託的事件。 確實,它們之間有着共性,就好比:「當事件發生的時候,與該事件相關聯的對象都將受到波及。」 因此,若是您瞭解C#中的事件,那將幫助您更好的理解「領域事件」。app
由此咱們能夠推導出:在領域驅動設計建模過程當中,若是發現有一項動做發生了以後,與之關聯的其餘領域對象將會受到波及。 那麼該動做可能就是「領域事件」。框架
光從概念上來說些許有些讓人頭暈,咱們來看看實際的一個例子:「當用戶將商品添加到購物車的時候,下方的推薦商品將爲他推薦同類型的商品」。 這是一個有先後發生關係的典型案例,商品被添加到了購物車就會引起推薦同類商品。 因此我們仔細來感覺一下這一個過程,抓一抓裏面的關鍵詞。「商品加入購物車」 就會致使 「推薦同類商品」。是否是和我們上面那一段的描述有些相似了? 因此仔細觀察以後,咱們能夠捕獲出一個領域對象來,該對象您可能將它命名爲(ProductAddedEvent)。dom
爲何咱們要將它命名爲過去時呢? 這也是印證了開頭那句話「動做發生了以後」。當該事件被捕獲了以後,就會將事件信息傳遞給「推薦商品」聚合根,執行相應處理邏輯。async
那麼事件的來源是哪裏呢?「用戶點擊」,「網頁響應」 這些都不是哦! 記住,咱們要深入關心領域對象,剛纔所說的狀況顯然與我們的領域對象一點兒關係也沒有。因此咱們能夠很天然的將目光轉向到「購物車」,「購物車」可能就是一個聚合根,它會有一個叫作「添加商品」的行爲,當該行爲完成以後就會引起一個「商品添加完成」的事件。
通過整理以後咱們可能會獲得一個這樣的流程:
因此您會發現,領域事件一方面充當了描述領域信息的做用,一方面承接了不一樣聚合根之間的交互。 固然事件不必定只有一個,被影響的領域對象也不必定只有一個。就比如「推薦商品」受到了「商品添加完成」事件以後,它本身也能產生一個另外的領域事件傳遞給下游。
到這裏您或許會感到使用領域事件和以往我們捕獲其餘對象不太同樣,好比捕獲值對象、實體等。由於對於領域事件來講,它多是「隱式」,咱們沒有直觀的感覺它的存在。
因此,請仔細的考慮這一點:當您要使用領域事件時,您將認同您的項目須要以事件做爲中心。 而項目中的各個領域對象都將以產生、發佈領域事件完成一系列的交互流程。
這裏我摘錄了《領域驅動設計模式、原理與實踐》中的一段話分享給你們:「領域事件將會在領域專家一塊兒進行的知識提煉環節中揭示出來。揭示領域事件是如此有價值,DDD實踐者都擁有創新的知識提煉技術來進行實踐以便讓其更專一於事件,好比事件風暴。不過,使用這些創新技術會帶來新的挑戰。既然概念化的模型都是以事件爲中心的,那麼代碼也須要以事件爲中心,以便它可以表述概念化模型。這就是領域事件設計模式所帶來的價值。」
因此在大多數時候您將感覺到項目逐漸具備 EDA(事件驅動架構)的風格。而此時,您可能會聯想到DDD中的另一種模式:事件溯源(EventSource),認爲本身必需要採用事件溯源來創建您的ddd項目。其實這並非必定的,採用領域事件和使用事件溯源是沒有直接關係的,雖然領域事件會幫助事件溯源完成的更好。
結合上面的介紹,您可能已經對發現領域事件有一點感受了。當聚合與聚合之間具備交互關係時,咱們每每會發現他們之間會存在某個領域事件來引起這系列行爲。
若是與領域專家交談時,發現了這樣的關鍵詞彙: 「當………………」、「若是A完成以後,那麼…………」,「發生…………的時候」。 這些詞彙可能在隱式的告訴您,該處也許存在着「領域事件」對象。
在使用領域事件以前,咱們必需要知道事件其實被劃分紅了:「內部」和「外部」。 就正如它的描述同樣,內部的領域事件發生在邊界以內,而外部的事件發生在邊界以外(好比微服務A產生了一個事件,而微服務B會受到該事件的影響)。
在Microsoft關於ESHOP案例的指導書籍《.NET 微服務 - 體系結構》 中,將其命名爲「領域事件和集成事件」:
該圖也形象的說明了基於一個邊界內的內部事件是如何交互的:
外部的事件每每須要一些基礎結構來實現遠程服務之間的進程間和分佈式通訊,好比rabbitMQ,kafka等。本篇文章重點講解內容爲內部的領域事件,關於外部的事件將會在後期《分佈式中的領域驅動設計》系列中爲你們介紹。
那麼是否個人DDD項目就必須使用「領域事件」呢? 也許您在網上歷來沒有見到過這樣的問題,所以也沒有該問題的確切性答案。關於該問題,我我的以爲答案是「不必定」。
就像上文說的同樣,若是您開始使用領域事件,那麼就證實您的項目和思惟將轉換爲「以事件做爲中心」。領域中大部分的交互都將以事件的方式來呈現。因此與其考慮「個人DDD項目就必須使用「領域事件」」這個問題,還不如轉換爲:「我是否須要用事件做爲中心來考慮問題?」。
因此,該問題的答案就取決於您本身了。這也是爲何您會在某些DDD框架或者DDD項目中沒有發現「領域事件」的緣由之一。
那麼,若是不使用事件來建模,聚合與聚合之間是如何進行交互的呢? 請看下文↓。
我利用搜索引擎進行了大量的查找,沒有發現任何關於「領域事件」 和 「領域服務」之間的對比內容。可是我認爲這二者卻有着不少類似的地方。 當Evans在初次提出領域驅動的概念時,是沒有考慮領域事件的,那麼也就意味着咱們可以經過原有的領域對象完成領域建模和業務流程。
回到剛纔那個問題,聚合與聚合之間只能經過事件完成操做嗎? 不必定。「領域服務」也承擔着領域對象與領域對象轉換的功能。
先回顧一下我們在領域服務章節瞭解到的部份內容:
當咱們發現一個操做沒法賦予一個實體或者值對象,且該操做又對業務流程很重要時,咱們每每須要使用領域服務
經過A和B,獲得一個C。
A須要一個繁瑣的內部策略才能獲得一個結果B。(ps: A,B,C指的是領域對象中的值對象或者實體)
因此這也意味着,領域服務內部能夠對多個領域對象(好比聚合根)進行操做。因此某些DDD框架將領域服務做爲完成流程操做的主要工具,容許使用者在領域服務中注入多個倉儲,從而對多個聚合根進行操做。
而「領域事件」呢,它經過發佈領域事件來達到不一樣領域對象的交互。
那麼到底應該使用「領域服務」仍是「領域事件」呢? 先回答本身是否須要引入事件模型。若是「是」,那麼請優先考慮使用領域事件。
這是很容易讓人頭暈的兩個對象,下面我將用兩句話讓您感覺他們的使用場景:
A:快遞在入庫時須要進行規格檢查,好比是否超重等
該場景,咱們除了引入「快遞」這一聚合根以外,沒有引入其餘領域對象。那麼此處的「檢查」操做,該行爲應該交給誰呢? 給「快遞」? 快遞本身檢查本身? 顯然不對,因此當某行爲不屬於一個實體或者值對象時,咱們就須要引入一個領域服務了。
B:當快遞被投遞到營業點時,證實快遞已經到達,配送員將打電話給用戶進行派送。
該場景中,咱們已經發現了有「快遞」、「營業點」、「快遞員」等領域對象,若是要完成一個「快遞到達」的用例,咱們會如何操做呢? 調用"營業點"的「收納進快遞」,而且接下來是調用「快遞員」的「配送快遞」。 此處涉及到多個聚合根之間的交互,那麼是選用領域服務仍是領域事件呢? 若是您基於事件建模,能夠採用領域事件,反之,您可使用領域服務。
若是您開始嘗試DDD項目,我建議您優先採用事件建模的方式。也就是說,考慮採用領域事件。將聚合根與聚合根之間的交互動做經過領域事件來傳達,而將領域對象的策略運算交由領域服務完成。更清晰的劃分它倆之間的職責。
實踐方案主要採用了Jimmy Bogard所提出的領域事件實現方案。聚合根中保持領域事件的集合,經過事件分配器將事件分配給對應的處理事件。
所以咱們能夠先創建幾個接口: IDomainEvent(代表該類爲領域事件)、IDomainEventHandler(用於攔截處理領域事件)、IEventDispatcher(事件分配器,將領域事件分發給處理程序)。
public interface IDomainEvent { } public interface IDomainEventHandler<in TDomainEvent> where TDomainEvent : IDomainEvent { Task HandleAysnc(TDomainEvent domainEvent, CancellationToken cancellationToken = default); } public interface IEventDispatcher { Task DispatchAsync<TDomainEvent>( TDomainEvent domainEvent, CancellationToken cancellationToken = default) where TDomainEvent :IDomainEvent; }
而後還須要給聚合根添加上一些方法,便於它可以保留領域事件在實例中:
public abstract class AggregateRoot<TKey> { public virtual TKey Id { get; set; } protected List<IDomainEvent> _domainEvents = new List<IDomainEvent>(); public virtual void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent); public virtual void RemoveDomainEvent(IDomainEvent domainEvent) => _domainEvents.Remove(domainEvent); public List<IDomainEvent> GetDomainEvents() => _domainEvents; }
最後,在倉儲進行持久化以前,經過事件分發器將保持在聚合根實例上的領域事件分發給對應的事件處理程序:
// EF Core DbContext public class OrderingContext : DbContext { public async Task<bool> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) { //Get aggregateRoot var aggregateRoots = dbContext.ChangeTracker.Entries().ToList(); // Dispatch Domain Events collection. await _eventDispatcher.DispatchAsync(aggregateRoots,cancellationToken); // After this line runs, all the changes (from the Command Handler and Domain // event handlers) performed through the DbContext will be committed var result = await base.SaveChangesAsync(); } }
因爲篇幅有限,上面的實現方案只是給了你們一個思路,因此缺乏了一些實現,若是您有須要能夠聯繫我,我提取一個小Demo上傳至Github。
關於另外的實現方案,您能夠查看微軟Eshop教程。
爲何我會建議您優先考慮使用領域事件呢? 爲了後期可以更容易的拆解項目爲微服務。 假如我們都是將聚合根之間的交互經過領域服務來完成,好比如今有一個領域服務A,它須要幫助聚合根A和聚合根B完成操做:
public class DomainServiceA { DomainServiceA(IRepositoryA repositoryA,IRepositoryB repositoryB); }
在該領域服務中,以來了聚合根A、B的存儲庫。如今A和B位於同一個服務中,這能夠很好的運行。可是若是有一天,B須要被獨立出去,單獨成爲一個服務怎麼辦呢? 該領域服務不得不進行更改。
而加入咱們經過領域事件來進行流轉,當聚合B被拆分出去以後,假如B須要A發佈的某個事件,那麼B只須要在本身的項目中添加一個該事件的類型就能夠了,而不須要修改其餘邏輯。(也許須要將內部事件轉換爲外部事件,可是核心業務代碼是不會更改的)。
因此構建項目初期,咱們在選型時要進行長遠的考慮。
本次咱們介紹了領域驅動設計中的領域事件。「若是捕獲領域事件?」,「DDD是否必定須要領域事件?」相信這些問題,看到這裏您內心已經有了本身的答案。
領域事件可以幫助咱們更好的描述領域中各個對象之間的狀態,就如同本文剛開始所說起到的觀點:「若是發現有一項動做發生了以後,與之關聯的其餘領域對象將會受到波及。」 將這些提取建模爲領域事件,將對您的項目帶來很好的收益。
感受每次講這個系列就比較嚴肅,若是您更喜歡輕鬆一些的內容能夠關注個人另一個系列《五分鐘的.NET》。
最後,偷偷說一句:創做不易,點個推薦吧.....