當前位置:
首頁 > 知識 > 領域事件與事件溯源的區別

領域事件與事件溯源的區別

為什麼領域事件domain events和事件溯源event sourcing不應混淆。

領域事件與事件溯源有什麼共同之處?

共同點是名稱中的「事件」一詞。但除此之外,在與項目,會議或培訓中的建築師和開發人員交談時,我經常聽到領域事件與事件溯源相關,事件溯源是領域事件的理想來源。在這篇博文中,我想概述為什麼我個人不贊同這個觀點。

在我爭論為什麼不分享這個觀點之前,我想確保充分了解領域事件與事件溯源:

領域事件

在領域驅動設計中,領域事件被描述為領域中發生的事情,這對領域專家很重要。無論領域在軟體系統中是否實現或在何種程度上實現多少,這種事件通常都會發生。它們也獨立於技術。因此,領域事件具有高價值語義,但是必須以領域專家所說的語言表達。例子可以是:

  • 用戶已註冊
  • 訂單已收到
  • 付款截止日期已過期

領域事件在有界上下文和跨域有界的上下文中都是相關的。領域事件也非常適合於通知其他有界上下文:關於在自己的有界上下文中發生的特定業務相關事件,從而以事件驅動的方式集成多個有界上下文。

事件溯源

Martin Fowler在其原始博客文章中描述了事件溯源的關鍵特徵如下:

事件溯源採用確保應用程序狀態的所有更改都存儲為一系列事件。

並不是直接在資料庫的表中按欄位保存應用程序中的當前狀態,然後在需要時重新載入狀態,後續如果應用中狀態更改就覆蓋這個數據表欄位值,現在事件溯源中是按時間順序排列事件列表,然後可以使用它來重建當前狀態、記憶中的狀態。

事件溯源是一般概念,但通常在與聚合相關的領域驅動設計的上下文裡面進行討論。因此,我使用聚合的持久性作為事件源的使用示例。

以下序列顯示了使用事件源來保持和恢復聚合狀態時的相關步驟:

1. 客戶端調用現有聚合上與業務相關的操作,此聚合已經保留了兩個先前的事件。

2.在處理調用之前,將創建聚合的空實例,並在聚合上重播先前保留的事件。聚合僅從相應事件中讀取狀態,並且不執行任何業務邏輯。完成後,聚合再次在內存中包含其當前狀態。

3.聚合接受客戶端的調用,根據其當前狀態進行驗證並進行處理,即執行相應的域邏輯。此時,聚合的內部狀態尚未更改 - 只有在處理調用期間創建的事件時才會執行此操作。

4.作為處理調用的結果,聚合生成一個事件(或多個事件),包括以後在聚合中重建狀態所需的狀態。該事件被持久化,以便它可以用於將來對此聚合的調用,以再次重建當前狀態。

通常會列出以下優點來使用事件溯源:

  • 存儲的事件不僅描述當前狀態,還描述了如何達到此狀態。
  • 通過僅在某個時間點重放事件,可以在任何時間重建過去的任何狀態。
  • 可以想像使用事件源來處理先前事件的不正確處理或延遲事件的到達。

話雖如此,事件溯源的實施也需要一定的概念和技術複雜性。一旦持久化,事件不應該改變,而領域邏輯通常會隨著時間的推移而發展。因此,代碼必須能夠處理非常舊的事件。快照是必要的,以便能夠以執行方式基於大事件歷史重建狀態。

此外,例如來自歐盟通用數據保護法規(GDPR)的要求的實施對事件溯源提出了真正的挑戰,因為事件溯源要求不刪除任何持久性事件。

事件溯源的事件≠領域事件

那麼為什麼我認為這兩個概念並不是那麼自然地融合在一起呢?

讓我們考慮以下示例:在自行車共享的域中,用戶想要註冊以租用自行車。當然,還必須支付費用,這是通過使用錢包的預付費方式完成的。

此域的上下文映射的相關部分可能如下所示:

領域事件與事件溯源的區別


註冊過程如下:

  • 用戶通過移動應用程序輸入他/她的手機號碼。
  • 用戶收到SMS代碼以確認電話號碼。
  • 用戶輸入確認碼。
  • 用戶輸入其他詳細信息(例如全名或地址)並完成註冊。

此過程UserRegistration在有界上下文中聚合實現Registration。用戶在註冊過程中多次與聚合UserRegistration的實例交互。UserRegistration逐步建立狀態,直到註冊成功完成。完成後,用戶應該可以為錢包充電並租一輛自行車。

現在,如果使用事件溯源來管理UserRegistration聚合的狀態,則會創建以下事件(包含相應的相關狀態)並隨著時間的推移而持久化:

  1. MobileNumberProvided (MobileNumber)
  2. VerificationCodeGenerated (VerificationCode)
  3. MobileNumberValidated (no additional state)
  4. UserDetailsProvided (FullName, Address, …)

這些事件足以讓UserRegistration在任何時間重建聚合的當前狀態。不需要額外的事件,特別是沒有表示註冊現已完成的事件。UserRegistration一旦接受UserDetailsProvided事件被處理,由於其內部域邏輯,聚合就知道這個事實。因此,一個UserRegistration實例可以隨時響應註冊是否已經完成。

此外,每個事件僅包含在重放期間能夠重建聚合狀態所必需的狀態。這通常只是受觸發事件的調用影響的狀態,即一種「差異」。從事件源的角度來看,在不受調用影響的事件上存儲附加狀態是沒有意義的。因此,即使顯式事件UserRegistrationCompleted持續存在,也不會包含任何其他狀態。

事件溯源的一些支持者投票表明,來自UserRegistration聚合的事件溯源的這些事件也可以發布到有界上下文內外的其他相關方,因此可以觸發進一步的域邏輯或更新其他狀態。在我們的示例中,這些將是兩個有界的上下文Accounting(用於初始化錢包)和Rental(用於創建註冊用戶)。

如果要使用來自事件源的事件來完成,則必須每個使用它的有界的上下文必須:

  • 處理這些細粒度事件並從UserRegistration聚合中知道至少部分域邏輯(例如,在用戶被認為完全註冊之後)。
  • 結合幾個事件來獲得用戶所需的整個狀態(例如來自的電話號碼MobileNumberProvided和其他詳細信息UserDetailsProvided)
  • 忽略對相應有界上下文不感興趣的事件(例如,VerificationCodeGenerated或MobileNumberValidated確認電話號碼)

從我的觀點來看,這種方法打破了系統不同部分之間的預期封裝,導致有界上下文之間的繁瑣通信,從而增加了有界上下文之間的耦合。主要原因是來自事件源的細粒度事件的語義在事件本身和相關信息(「有效載荷」)方面都太低級。

在我看來,如果想改善事情,UserRegistration發布領域事件UserRegistrationCompleted時應該將所有相關信息MobileNumber,FullName以及Address(例如VerificationCode登記已成功完成後)作為有效載荷。該領域事件具有適當的語義,易於由外部有界上下文處理,而不必知道註冊過程的任何內部。

在某些情況下,事件源的事件語義肯定可以提供適當的語義,以便外部消費者能夠以簡單的方式處理(例如MobileNumberProvided,想要了解所有電話??的假設消費者的事件)已註冊的數字)。但即便如此,我還是選擇將事件溯源和事件的實現分開,以便它們可以相互獨立地進化。這意味著在系統中輸入的領域事件中電話號碼將有兩個表示形式,每個表示具有不同的用途。

事件溯源和CQRS

那麼來自事件溯源的事件是否只能在相應的聚合中使用?

從我的角度來看,基本上是的。然而,一個可能且有意義的例外是將這些事件與CQRS中的讀取模型結合使用。當然,這也會影響封裝,但我的經驗表明,CQRS的讀取模型通常與聚合相關聯,因為它們提供了對該聚合數據的特定視圖。因此,人們可能會爭辯說,在讀取模型中處理細粒度事件所產生的耦合是可以接受的。

結論

我將事件源視為狀態持久性的實現策略,例如聚合。不應將此策略暴露在聚合的邊界之外。因此,事件源的事件應僅在相應的聚合內部或CQRS的上下文中使用,以構建相關的讀取模型。

另一方面,領域事件表示與聚合的持久性策略的類型無關的特定事實或事件,例如,用於集成有界上下文。

事件溯源和領域事件當然可以同時使用,但不應相互影響。這兩個概念用於不同的目的,因此不應混合使用。

(banq註:領域事件和事件溯源的事件應該盡量統一,因為如果事件溯源中的事件不是代表業務事件,那麼我們就不必關注業務狀態了,但是這是不可能的。)

作者:JDON

原文:https://www.jdon.com/51532

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

SQL語法和C井調用
解讀gcc和g++編譯器分別對c與c++文件影響

TAG:程序員小新人學習 |