DDD理論學習系列(9)-- 領域事件

来源:http://www.cnblogs.com/sheng-jie/archive/2017/07/06/7124727.html
-Advertisement-
Play Games

"DDD理論學習系列——案例及目錄" 1. 引言 A domain event is a full fledged part of the domain model, a representation of something that happened in the domain. Ignore ...


DDD理論學習系列——案例及目錄


1. 引言

A domain event is a full-fledged part of the domain model, a representation of something that happened in the domain. Ignore irrelevant domain activity while making explicit the events that the domain experts want to track or be notified of, or which are associated with state change in the other model objects.
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明確領域專家要跟蹤或希望被通知的事情,或與其他模型對象中的狀態更改相關聯。

針對官方釋義,我們可以理出以下幾個要點:

  1. 領域事件作為領域模型的重要部分,是領域建模的工具之一。
  2. 用來捕獲領域中已經發生的事情。
  3. 並不是領域中所有發生的事情都要建模為領域事件,要忽略無業務價值的事件。
  4. 領域事件是領域專家所關心的(需要跟蹤的、希望被通知的、會引起其他模型對象改變狀態的)發生在領域中的一些事情。

簡而言之,領域事件是用來捕獲領域中發生的具有業務價值的一些事情。它的本質就是事件,不要將其複雜化。在DDD中,領域事件作為通用語言的一種,是為了清晰表述領域中產生的事件概念,幫助我們深入理解領域模型。

2. 認識領域事件

當用戶在購物車點擊結算時,生成待付款訂單,若支付成功,則更新訂單狀態為已支付,扣減庫存,並推送撿貨通知信息到撿貨中心。

在這個用例中,“訂單支付成功”就是一個領域事件。

考慮一下,在你沒有接觸領域事件或EDA(事件驅動架構)之前,你會如何實現這個用例。肯定是簡單直接的方法調用,在一個事務中分別去調用狀態更新方法、扣減庫存方法、發送撿貨通知方法。這無可厚非,畢竟之前都是這樣乾的。

那這樣設計有什麼問題?

  1. 試想一下,若現在要求支付成功後,需要額外發送一條付款成功通知到微信公眾號,我們怎麼實現?想必我們需要額外定義發送微信通知的介面並封裝參數,然後再添加對方法的調用。這種做法雖然可以解決需求的變更,但很顯然不夠靈活耦合性強,也違反了OCP。
  2. 將多個操作放在同一個事務中,使用事務一致性可以保證多個操作要麼全部成功要麼全部失敗。在一個事務中處理多個操作,若其中一個操作失敗,則全部失敗。但是,這在業務上是不允許的。客戶成功支付了,卻發現訂單依舊為待付款,這會導致糾紛的。
  3. 違反了聚合的一大原則:在一個事務中,只對一個聚合進行修改。在這個用例中,很明顯我們在一個事務中對訂單聚合和庫存聚合進行了修改。

那如何解決這些問題?我們可以藉助領域事件的力量。

  1. 解耦,可以通過發佈訂閱模式,發佈領域事件,讓訂閱者自行訂閱;
  2. 通過領域事件來達到最終一致性,提高系統的穩定性和性能;
  3. 事件溯源;
  4. 等等。

下麵我們就來一一深入。

3.建模領域事件

如何使用領域事件來解耦呢?
當然是封裝不變,應對萬變。那針對上面的用例,不變的是什麼,變的又是什麼?不變的是訂單支付成功這個事件;變化的是針對這個事件的不同處理手段。

而我們要如何封裝呢?
這時我們就要理清事件的本質,事件有因必有果,事件是由事件源和事件處理組合而成的。通過事件源我們來辨別事件的來源,事件處理來表示事件導致的下一步操作。

3.1. 抽象事件源

事件源應該至少包含事件發生的時間和觸發事件的對象。我們提取IEventData介面來封裝事件源:

/// <summary>
/// 定義事件源介面,所有的事件源都要實現該介面
/// </summary>
public interface IEventData
{
    /// <summary>
    /// 事件發生的時間
    /// </summary>
    DateTime EventTime { get; set; }

    /// <summary>
    /// 觸發事件的對象
    /// </summary>
    object EventSource { get; set; }
}

通過實現IEventData我們可以根據自己的需要添加自定義的事件屬性。

3.2. 抽象事件處理

針對事件處理,我們提取一個IEventHandler介面:

 /// <summary>
 /// 定義事件處理器公共介面,所有的事件處理都要實現該介面
 /// </summary>
 public interface IEventHandler
 {
 }

事件處理要與事件源進行綁定,所以我們再來定義一個泛型介面:

 /// <summary>
 /// 泛型事件處理器介面
 /// </summary>
 /// <typeparam name="TEventData"></typeparam>
 public interface IEventHandler<TEventData> : IEventHandler where TEventData : IEventData
 {
     /// <summary>
     /// 事件處理器實現該方法來處理事件
     /// </summary>
     /// <param name="eventData"></param>
     void HandleEvent(TEventData eventData);
 }

以上,我們就完成了領域事件的抽象。在代碼中我們通過實現一個IEventHandler<T>來表達領域事件的概念。

3.3. 領域事件的發佈和訂閱

領域事件不是無緣無故產生的,它有一個發佈方。同理,它也要有一個訂閱方。

那如何和訂閱和發佈領域事件呢?
領域事件的發佈可以使用發佈--訂閱模式來實現。而比較常見的實現方式就是事件匯流排

事件匯流排是一種集中式事件處理機制,允許不同的組件之間進行彼此通信而又不需要相互依賴,達到一種解耦的目的。Event Bus就相當於一個介於Publisher(發佈方)和Subscriber(訂閱方)中間的橋梁。它隔離了Publlisher和Subscriber之間的直接依賴,接管了所有事件的發佈和訂閱邏輯,並負責事件的中轉。

這裡就簡要說明一下事件匯流排的實現的要點:

  1. 事件匯流排維護一個事件源與事件處理的映射字典;
  2. 通過單例模式,確保事件匯流排的唯一入口;
  3. 利用反射或依賴註入完成事件源與事件處理的初始化綁定;
  4. 提供統一的事件註冊、取消註冊和觸發介面。

最後,我們看下事件匯流排的介面定義:

public interface IEventBus
 {
    void Register < TEventData > (IEventHandler eventHandler);

    void UnRegister < TEventData > (Type handlerType) where TEventData: IEventData;

    void Trigger < TEventData > (Type eventHandlerType, TEventData eventData) where TEventData: IEventData;
}

在應用服務和領域服務中,我們都可以直接調用Register方法來完成領域事件的註冊,調用Trigger方法來完成領域事件的發佈。

而關於事件匯流排的具體實現,可參考我的這篇博文——事件匯流排知多少

4. 最終一致性

說到一致性,我們要先搞明白下麵幾個概念。

事務一致性
事務一致性是是資料庫事務的四個特性之一,也就是ACID特性之一:

原子性(Atomicity):事務作為一個整體被執行,包含在其中的對資料庫的操作要麼全部被執行,要麼都不執行。
一致性(Consistency):事務應確保資料庫的狀態從一個一致狀態轉變為另一個一致狀態。
隔離性(Isolation):多個事務併發執行時,一個事務的執行不應影響其他事務的執行。
持久性(Durability):已被提交的事務對資料庫的修改應該永久保存在資料庫中。

我們用一張圖來理解一下:

事務一致性
在事務一致性的保證下,上面的圖示只會有兩個結果:

  1. A和B兩個操作都成功了。
  2. A和B兩個操作都失敗了。

數據一致性
舉個簡單的例子,假設10個人,每人有100個虛擬幣,虛擬幣僅能在這10人內流通,不管怎麼流通,最終的虛擬幣總數都是1000個,這就是數據一致性。

領域一致性
簡單理解就是在領域中的操作要滿足領域中定義的業務規則。比如你轉賬,並不是你餘額充足就可以轉賬的,還要求賬戶的狀態為非掛失、鎖定狀態。

回到我們的案例,當支付成功後,更新訂單狀態,扣減庫存,併發送撿貨通知。按照我們以往的做法,為了維護訂單和庫存的數據一致性,我們將這三個操作放到一個應用服務去做(因為應用服務管理事務),事務的一致性可以保證要麼全部成功要麼全部失敗。但是,試想一下,客戶支付成功後,訂單依舊為待付款狀態,這會引起糾紛。另外,由於庫存沒有及時扣減,很可能會導致庫存超賣。怎麼辦呢?
將事務拆解,使用領域事件來達到最終一致性。

最終一致性
“最終一致性”是一種設計方法,可以通過將某些操作的執行延遲到稍後的時間來提高應用程式的可擴展性和性能。

最終一致性

對於常見於分散式系統的最終一致性工作流中,客戶同樣在系統中執行一個命令,但這個系統只為維護事務中的領域一致性運行部分的操作,剩餘的操作在允許延後執行。針對上圖的結果:

  1. A操作執行成功,B操作將延後執行。
  2. A操作失敗,B操作將不會執行。

而針對我們的案例,我們如何使用領域事件來進行事務拆分呢?我們看下下麵這張圖你就明白了。

領域事件在最終一致性的位置

分析一下,針對我們案例,我們發現一個用例需要修改多個聚合根的情況,並且不同的聚合根還處於不同的限界上下文中。其中訂單和庫存均為聚合根,分別屬於訂單系統和庫存系統。我們可以這樣做:

  1. 在訂單所在的聚合根中更新訂單支付狀態,併發布“訂單成功支付”的領域事件;
  2. 然後庫存系統訂閱並處理庫存扣減邏輯;
  3. 通知系統訂閱並處理撿貨通知。

通過這種方式,我們即保證了聚合的原則,又保證了數據的最終一致性。

5. 事件存儲和事件溯源

關於事件存儲(Event Store)和事件溯源(Event Sourcing)是一個比較複雜的概念,我們這裡就簡單介紹下,不做過多展開,後續再設章節詳述。

事件存儲,顧名思義,即事件的持久化。那為什麼要持久化事件?

  1. 當事件發佈失敗時,可用於重新發佈。
  2. 通過消息中間件去分發事件,提高系統的吞吐量。
  3. 用於事件溯源。

源代碼管理工具我們都用過,如Git、TFS、SVN等,通過記錄文件每一次的修改記錄,以便我們跟蹤每一次對源代碼的修改,從而我們可以隨時回滾到文件的指定修改版本。

事件溯源的本質亦是如此,不過它存儲的並非聚合每次變化的結果,而是存儲應用在該聚合上的歷史領域事件。當需要恢復某個狀態時,需要把應用在聚合的領域事件按序“重放”到要恢復狀態對應的領域事件為止。

6.總結

經過上面的分析,我們知道引入領域事件的目的主要有兩個,一是解耦,二是使用領域事件進行事務的拆分,通過引入事件存儲,來實現數據的最終一致性。

最後,對於領域事件,我們可以這樣理解:
通過將領域中所發生的活動建模成一系列的離散事件,並將每個事件都用領域對象來表示,來跟蹤領域中發生的事情。
也可以簡要理解為:領域事件 = 事件發佈 + 事件存儲 + 事件分發 + 事件處理

以上,僅是個人理解,DDD水很深,剪不斷,理還亂,有問題或見解,歡迎指正交流。

參考資料:
在微服務中使用領域事件
使用聚合、事件溯源和CQRS開發事務型微服務
如何理解資料庫事務中的一致性的概念?
Eventual Consistency via Domain Events and Azure Service Bus


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 題目描述 給出一個只由小寫英文字元a,b,c...y,z組成的字元串S,求S中最長迴文串的長度. 字元串長度為n 輸入輸出格式 輸入格式: 一行小寫英文字元a,b,c...y,z組成的字元串S 輸出格式: 一個整數表示答案 輸入輸出樣例 輸入樣例#1: aaa 輸出樣例#1: 3 輸入樣例#1: a ...
  • 一、獲取流程 1、獲取 access_token 2、通過access_token換取 jsapi_ticket 3、簽名演算法 簽名生成規則如下:參與簽名的欄位包括noncestr(隨機字元串), 有效的jsapi_ticket, timestamp(時間戳), url(當前網頁的URL,不包含#及 ...
  • 今天接觸到一種很玄幻的東西: 差分約束 個人的理解:差分約束就是給定一些限制條件,求出滿足條件的最優解,或者判斷條件是否成立 做法/思路: 1.首先根據題目的條件,寫出相應的不等式 2.將不等式轉換成a-b<=c的形式 3.建一條權值為c的邊,從b指向a 4.從0點向其他點連一條邊權為1的點 5.跑 ...
  • 題目描述 小 K 在 Minecraft 裡面建立很多很多的農場,總共 n 個,以至於他自己都忘記了每個 農場中種植作物的具體數量了,他只記得一些含糊的信息(共 m 個),以下列三種形式描 述: 農場 a 比農場 b 至少多種植了 c 個單位的作物。 農場 a 比農場 b 至多多種植了 c 個單位的 ...
  • Python中字典和集合 映射類型: 表示一個任意對象的集合,且可以通過另一個幾乎是任意鍵值的集合進行索引 與序列不同,映射是無序的,通過鍵進行索引 任何不可變對象都可用作字典的鍵,如字元串、數字、元組等 包含可變對象的列表、字典和元組不能用作鍵 引用不存在的鍵會引發KeyError異常 1)字典 ...
  • 通過添加powershell插件後,使用它強大的windows系統命令,就把發佈好的程式包推送到具體的應用伺服器了。 系統管理-插件管理-powershell 把它安裝,重啟jenkins,然後修改你之前的job,把powershell的推送文件腳本加上 添加一個構建類型,在msbuild下麵添加 ...
  • Jenkins是一個持續集成的環境,它是java開發的,大叔認為它的工作流程是 從源代碼拉一個項目下來到它本地(可以配置定時機制) 恢復相關程式包nuget 編譯程式 發佈程式 現在說一下在配置jenkins里要註意的幾個地方: jenkins的構建工作目錄和job目錄說明 構建目錄:C:\Prog ...
  • 本章內容還在整理上傳中,你可以等全部更新完畢後再查閱也可以先預覽已上傳的內容。。。。。。 7. 應用層的命令模式 在上個章節里我們設計並編碼了領域對象Permission,但是目前Permission並沒有任何行為上的設計。這是因為我們不建議“憑空去製造行為”,而是在領域對象第一個版本的代碼實現之後 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...