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.
領域事件是一個領域模型中極其重要的部分,用來表示領域中發生的事件。忽略不相關的領域活動,同時明确領域專家要跟蹤或希望被通知的事情,或與其他模型對象中的狀态更改相關聯。
針對官方釋義,我們可以理出以下幾個要點:
- 領域事件作為領域模型的重要部分,是領域模組化的工具之一。
- 用來捕獲領域中已經發生的事情。
- 并不是領域中所有發生的事情都要模組化為領域事件,要忽略無業務價值的事件。
- 領域事件是領域專家所關心的(需要跟蹤的、希望被通知的、會引起其他模型對象改變狀态的)發生在領域中的一些事情。
簡而言之,領域事件是用來捕獲領域中發生的具有業務價值的一些事情。它的本質就是事件,不要将其複雜化。在DDD中,領域事件作為通用語言的一種,是為了清晰表述領域中産生的事件概念,幫助我們深入了解領域模型。
2. 認識領域事件
當使用者在購物車點選結算時,生成待付款訂單,若支付成功,則更新訂單狀态為已支付,扣減庫存,并推送撿貨通知資訊到撿貨中心。
在這個用例中,“訂單支付成功”就是一個領域事件。
考慮一下,在你沒有接觸領域事件或EDA(事件驅動架構)之前,你會如何實作這個用例。肯定是簡單直接的方法調用,在一個事務中分别去調用狀态更新方法、扣減庫存方法、發送撿貨通知方法。這無可厚非,畢竟之前都是這樣幹的。
那這樣設計有什麼問題?
- 試想一下,若現在要求支付成功後,需要額外發送一條付款成功通知到微信公衆号,我們怎麼實作?想必我們需要額外定義發送微信通知的接口并封裝參數,然後再添加對方法的調用。這種做法雖然可以解決需求的變更,但很顯然不夠靈活耦合性強,也違反了OCP。
- 将多個操作放在同一個事務中,使用事務一緻性可以保證多個操作要麼全部成功要麼全部失敗。在一個事務中處理多個操作,若其中一個操作失敗,則全部失敗。但是,這在業務上是不允許的。客戶成功支付了,卻發現訂單依舊為待付款,這會導緻糾紛的。
- 違反了聚合的一大原則:在一個事務中,隻對一個聚合進行修改。在這個用例中,很明顯我們在一個事務中對訂單聚合和庫存聚合進行了修改。
那如何解決這些問題?我們可以借助領域事件的力量。
- 解耦,可以通過釋出訂閱模式,釋出領域事件,讓訂閱者自行訂閱;
- 通過領域事件來達到最終一緻性,提高系統的穩定性和性能;
- 事件溯源;
- 等等。
下面我們就來一一深入。
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之間的直接依賴,接管了所有事件的釋出和訂閱邏輯,并負責事件的中轉。
這裡就簡要說明一下事件總線的實作的要點:
- 事件總線維護一個事件源與事件處理的映射字典;
- 通過單例模式,確定事件總線的唯一入口;
- 利用反射或依賴注入完成事件源與事件處理的初始化綁定;
- 提供統一的事件注冊、取消注冊和觸發接口。
最後,我們看下事件總線的接口定義:
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):已被送出的事務對資料庫的修改應該永久儲存在資料庫中。
我們用一張圖來了解一下:
在事務一緻性的保證下,上面的圖示隻會有兩個結果:
- A和B兩個操作都成功了。
- A和B兩個操作都失敗了。
資料一緻性
舉個簡單的例子,假設10個人,每人有100個虛拟币,虛拟币僅能在這10人内流通,不管怎麼流通,最終的虛拟币總數都是1000個,這就是資料一緻性。
領域一緻性
簡單了解就是在領域中的操作要滿足領域中定義的業務規則。比如你轉賬,并不是你餘額充足就可以轉賬的,還要求賬戶的狀态為非挂失、鎖定狀态。
回到我們的案例,當支付成功後,更新訂單狀态,扣減庫存,并發送撿貨通知。按照我們以往的做法,為了維護訂單和庫存的資料一緻性,我們将這三個操作放到一個應用服務去做(因為應用服務管理事務),事務的一緻性可以保證要麼全部成功要麼全部失敗。但是,試想一下,客戶支付成功後,訂單依舊為待付款狀态,這會引起糾紛。另外,由于庫存沒有及時扣減,很可能會導緻庫存超賣。怎麼辦呢?
将事務拆解,使用領域事件來達到最終一緻性。
最終一緻性
“最終一緻性”是一種設計方法,可以通過将某些操作的執行延遲到稍後的時間來提高應用程式的可擴充性和性能。
對于常見于分布式系統的最終一緻性工作流中,客戶同樣在系統中執行一個指令,但這個系統隻為維護事務中的領域一緻性運作部分的操作,剩餘的操作在允許延後執行。針對上圖的結果:
- A操作執行成功,B操作将延後執行。
- A操作失敗,B操作将不會執行。
而針對我們的案例,我們如何使用領域事件來進行事務拆分呢?我們看下下面這張圖你就明白了。
分析一下,針對我們案例,我們發現一個用例需要修改多個聚合根的情況,并且不同的聚合根還處于不同的限界上下文中。其中訂單和庫存均為聚合根,分别屬于訂單系統和庫存系統。我們可以這樣做:
- 在訂單所在的聚合根中更新訂單支付狀态,并釋出“訂單成功支付”的領域事件;
- 然後庫存系統訂閱并處理庫存扣減邏輯;
- 通知系統訂閱并處理撿貨通知。
通過這種方式,我們即保證了聚合的原則,又保證了資料的最終一緻性。
5. 事件存儲和事件溯源
關于事件存儲(Event Store)和事件溯源(Event Sourcing)是一個比較複雜的概念,我們這裡就簡單介紹下,不做過多展開,後續再設章節詳述。
事件存儲,顧名思義,即事件的持久化。那為什麼要持久化事件?
- 當事件釋出失敗時,可用于重新釋出。
- 通過消息中間件去分發事件,提高系統的吞吐量。
- 用于事件溯源。
源代碼管理工具我們都用過,如Git、TFS、SVN等,通過記錄檔案每一次的修改記錄,以便我們跟蹤每一次對源代碼的修改,進而我們可以随時復原到檔案的指定修改版本。
事件溯源的本質亦是如此,不過它存儲的并非聚合每次變化的結果,而是存儲應用在該聚合上的曆史領域事件。當需要恢複某個狀态時,需要把應用在聚合的領域事件按序“重放”到要恢複狀态對應的領域事件為止。
6.總結
經過上面的分析,我們知道引入領域事件的目的主要有兩個,一是解耦,二是使用領域事件進行事務的拆分,通過引入事件存儲,來實作資料的最終一緻性。
最後,對于領域事件,我們可以這樣了解:
通過将領域中所發生的活動模組化成一系列的離散事件,并将每個事件都用領域對象來表示,來跟蹤領域中發生的事情。
也可以簡要了解為:領域事件 = 事件釋出 + 事件存儲 + 事件分發 + 事件處理。
以上,僅是個人了解,DDD水很深,剪不斷,理還亂,有問題或見解,歡迎指正交流。
參考資料:
在微服務中使用領域事件
使用聚合、事件溯源和CQRS開發事務型微服務
如何了解資料庫事務中的一緻性的概念?
Eventual Consistency via Domain Events and Azure Service Bus
推薦連結:你必須知道的.NET Core開發指南
推薦連結:你必須知道的ML.NET開發指南
推薦連結:你必須知道的Office開發指南
推薦連結:你必須知道的IOT開發指南
推薦連結:你必須知道的Azure基礎知識
推薦連結:你必須知道的PowerBI基礎知識

關注我的公衆号『微服務知多少』,我們微信不見不散。
閱罷此文,如果您覺得本文不錯并有所收獲,請【打賞】或【推薦】,也可【評論】留下您的問題或建議與我交流。
你的支援是我不斷創作和分享的不竭動力!
作者:『聖傑』
出處:http://www.cnblogs.com/sheng-jie/
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利。