DDD:不要洩露領域事件

banq發表於2024-12-15


領域事件必須保持私密。

耦合是所有問題的根源。隨著時間的推移,它會讓事情變得更加困難,在某些情況下甚至是不可能的。
耦合是造成您的壓力和技術團隊效率低下的罪魁禍首。
簡而言之,耦合是邪惡的,必須與之鬥爭!

開始之前:為什麼要使用 DDD?
我們可以列出很多好處,但我將特意強調幾個主要的好處:

  • 團隊之間的協調:DDD 促使我們這些技術人員避免將所有技術限制強加給其他人(非技術人員)。它鼓勵我們瞭解業務、協作並與業務專家一起建模。
  • 減少複雜性或過度工程:透過專注於核心領域,我們將大部分精力投入到最關鍵的方面——那些為業務帶來最大價值的方面。技術只是一個手段,而不是起點。在進入微服務、Docker、框架等之前,我們會先考慮“業務”。
  • 改善溝通:無處不在的語言:在技術人員和非技術人員之間,我們使用同一種語言——業務語言。無需同時將技術概念翻譯成業務術語,反之亦然。這減少了心理負擔和誤解。
  • 更大的靈活性和可擴充套件性:透過對業務進行建模,我們為自己開啟了一扇大門,這在業務發展時變得更加重要。今天適用的事情明天可能就不適用了。這種分離允許系統在不破壞一切的情況下發展——或者至少不會破壞整個設定。
  • 降低風險:不再開發不必要的功能。相反,團隊(技術和非技術)共享清晰的願景,在優先事項上保持一致,並瞭解利害關係。

那麼什麼是“領域事件”?
領域事件代表了我們業務中發生的不可否認的事實。其命名約定通常使用過去時,並且通常由唯一識別符號(UUID、ULID 等)標識。它通常包含一些屬性(儘管這不是強制性的)。

例子:
SubscriptionCanceled一旦客戶取消訂閱,就會發出該事件。該事件的有效負載可能包括:

  • 事件 ID
  • 客戶的唯一識別符號
  • 相關訂閱的唯一識別符號
  • 取消日期
  • 可選的取消原因

值得注意的是,該事件可能屬於一個有界上下文BC(設定上下文,以下或稱BC或界限上下文),例如“訂閱”:

<font>// BoundedContext/Subscription/Domain/Events/SubscriptionCanceled <i>

 “id” : “de305d54-75b4-431b-adb2-eb6b9e546013” ,
 “userId” : “123e4567-e89b-12d3-a456-426614174000” ,
 “subscriptionId” : “f47ac10b-58cc-4372-a567-0e02b2c3d479” ,
 “canceledAt” : “2024-12-01T14:30:00 + 01:00” ,
 “cancelationReason” : null
}

是否使用外BC產生的事件?
假設有一個“計費”限界上下文BC對事件感興趣SubscriptionCanceled— 例如,停止每月向客戶計費。您可能會想監聽“訂閱”BC發出的事件,並在“計費”系統中進行必要的更改。

這看起來很簡單,對吧?但通常,當某件事讓人感覺太簡單時,是時候退後一步,花點時間分析一下情況了。

從另一個業務上下文消費領域事件會直接將您的上下文與他們的上下文耦合。

  • 正如我們之前所說,耦合是邪惡的!

那麼,我們該如何消費這個活動事件呢?
首先,這個領域事件甚至不應該在其上下文之外使用

無論您使用的是模組化方法(單個應用程式中的多個服務)還是分散式微服務,任何人都不應該能夠直接使用您的領域事件,除非至少明確告知不允許這樣做。

向外部公開領域事件會與使用它的人建立隱性契約

  • 最好的情況是:你知道消費者是誰。
  • 最壞的情況是:你不知道。

您將失去對事件的控制和管理權,因為每次更改都有可能破壞合同,從而可能破壞系統的穩定性。
在最壞的情況下,一切都可能像紙牌屋一樣倒塌。

更好的方法是明確你的合同。如果你仍然想發出事件,你應該建立具有明確定義的合同的公共事件,這樣你就可以堅持下去而不會失去自由。

工作原理如下:在有BC上下文的基礎設施層中,您可以新增一個監聽域事件的訂閱者

  • 在此階段,一切仍在您的BC上下文中發生,因此完全沒問題。
  • 然後,該訂閱者會對您的事件做出反應,並在公共事件匯流排上排程一個公共事件(例如SubscriptionCanceled)

很多事情都變了!
此公共事件專門設計為公開。其契約在首次釋出時定義。這意味著有效載荷不能任意更改。如果需要修改或刪除屬性,您必須通知消費者。

考慮到這一點,您可以新增一個簡單的屬性來向消費者表明此事件是 1.0 版本,使每個人都能清楚且易於管理。

<font>//BoundedContext/Subscription/Infrastrucuture/Events/SubscriptionCanceled <i>

 “domainEventVersion” : 1 ,
 “id” : “550e8400-e29b-41d4-a716-446655440000” ,
 “userId” : “123e4567-e89b-12d3-a456-426614174000” ,
 “subscriptionId” : “f47ac10b-58cc-4372-a567-0e02b2c3d479” ,
 “canceledAt” : “2024-12-01T14:30:00 + 01:00” ,
 “cancelationReason” : null
}

業務需求變更發生
現在讓我們考慮一個經常發生的場景:您的業務在發展,因此您的領域事件也在發展。

例如,以前可選的取消訂閱原因不再是自由格式的字串 — — 而且它不再是可選的。
現在,它是一種取消型別,可能還附帶客戶的評論。

我們的領域事件的更新版本可能如下所示:

<font>// BoundedContext/Subscription/Domain/Events/SubscriptionCanceled <i>

 
"id" :  "de305d54-75b4-431b-adb2-eb6b9e546013"
 
"userId" :  "123e4567-e89b-12d3-a456-426614174000"
 
"subscriptionId" :  "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 
"reason" :  "太貴了" ,  // 型別的字串版本(例如 SubscriptionCancelReasonType)<i>
 
"comment" :  "最近的增長是一種濫用!" 
}

我們保持靈活性,但不會違反合同。

為了適應變化,同時保持我們的系統靈活性並且不違反現有合同,我們可以調整我們的基礎設施。

在基礎設施層中,我們將更新監聽器,將新的取消型別和客戶評論連線成一個字串,格式如下:

"<CancelReasonType> : <Optional Customer comment>"

此外,我們將藉此機會將現有事件移至v1目錄,以明確標明其版本。這讓我們能夠推出新版本的活動,同時保持與原始版本的向後相容性。

<font>// BoundedContext/Subscription/Infrastrucuture/Events/v1/SubscriptionCanceled <i>

 
"id" :  "550e8400-e29b-41d4-a716-446655440000"
 
"domainEventVersion" :  1.1 ,  // 增加 .1 表示有新屬性<i>
 
"userId" :  "123e4567-e89b-12d3-a456-426614174000"
 
"subscriptionId" :  "f47ac10b-58cc-4372-a567-0e02b2c3d479"
 
"canceledAt" :  "2024-12-01T14:30:00+01:00"
 
"cancelationReason" :  "[Too昂貴]:最近的增長是一種濫用!“ 
}

接下來,我們將建立公共活動的新版本,如下所示:

<font>// BoundedContext/Subscription/Infrastrucuture/Events/v2/SubscriptionCanceled <i>

 “id” :  “9b1deb4d-72f1-4f24-9106-95e8c31b5d0b” , 
 “domainEventVersion” :  2 , 
 “userId” :  “123e4567-e89b-12d3-a456-426614174000” , 
 “subscriptionId” :  “f47ac10b-58cc-4372-a567-0e02b2c3d479” , 
 “canceledAt” :  “2024-12-01T14:30:00+01:00” , 
 “cancelationReason” :  “[太貴了]” , 
 “customerComment” :  “最近的漲幅是濫用!“ 
}

我們可以看到,每次釋出都會發布兩個公共事件。我們在建立新版本的合同時保留了上一個版本的合同。


如何向消費者告知新版本?
最簡單的技術之一是向您的合約新增一個屬性,表明該事件已被棄用,應改用最新支援的版本。

在我們的公共事件版本 1 中,我們可以簡單地新增一個屬性,例如deprecatedDomainEvent true。此外,我們可以包含另一個屬性,例如useDomainEventVersionInstead: 2。

<font>// BoundedContext/Subscription/Infrastructure/Events/v1/SubscriptionCanceled <i>

 “domainEventVersion” :  1.2 , 
 “deprecatedDomainEvent” :  true
 “useDomainEventVersionInstead” :  2 , 
 “userId” :  “123e4567-e89b-12d3-a456-426614174000” , 
 “subscriptionId” :  “f47ac10b-58cc-4372-a567-0e02b2c3d479” , 
 “canceledAt” :  “2024-12-01T14:30:00+01:00” , 
 “cancelationReason” :  “[太貴了]:最近的漲價是濫用!” 
}

您可能會想:新增這些屬性不會破壞合同嗎?
其實不會 — 因為在這裡,我們不會破壞系統。新增屬性不會帶來副作用,而修改或刪除可能會。

通常,允許在已經公開的事件中新增附加屬性是可以接受的。

關於十進位制事件版本,我同意,我在這裡走捷徑,但這只是為了簡化帖子。如果你理解整個概念,你就會知道該怎麼做:)

改善我們的公共活動事件
到目前為止提供的示例都比較基礎,並未完全針對生產進行最佳化。
常見的最佳做法是將資料分為兩個主要屬性:

  • data:包含特定於事件的所有屬性。
  • metadata:包含事件的所有技術屬性。

這是我們活動事件的改進版本:

// BoundedContext/Subscription/Infrastructure/Events/v1/SubscriptionCanceled 

 "data" :  { 
  "userId" :  "123e4567-e89b-12d3-a456-426614174000" , 
  "subscriptionId" :  "f47ac10b-58cc-4372-a567-0e02b2c3d479" , 
  "canceledAt" :  "2024-12-01T14:30:00+01:00" , 
  "cancelationReason" :  "[太貴了] : 最近的漲價是濫用!" } 
 } , 
 “metadata” :  { 
  “id” :  “550e8400-e29b-41d4-a716-446655440000” , 
  “型別” :  “SubscriptionHasBeenCanceled” , 
  “域” :  { 
   “id” :  “de305d54-75b4-431b-adb2-eb6b9e546013” , 
   “事件版本” :  1.3 , 
   “已棄用” :  true , 
   “useInstead” :  2.1 
  } 
 } 
}

// BoundedContext/Subscription/Infrastructure/Events/v2/SubscriptionCanceled 

 “data” :  { 
  “userId” :  “123e4567-e89b-12d3-a456-426614174000” , 
  “subscriptionId” :  “f47ac10b-58cc-4372-a567-0e02b2c3d479” , 
  “canceledAt” :  “2024-12-01T14:30:00+01:00” , 
  “cancelationReason” :  “[太貴了]” , 
  “customerComment” :  “最近的漲價是濫用!” 
 } ,
 "metadata" : { 
  "id" : “9b1deb4d-72f1-4f24-9106-95e8c31b5d0b” ,
  "型別" : “SubscriptionHasBeenCanceled” ,
  "域" : { 
   "id" : “de305d54-75b4-431b-adb2-eb6b9e546013” ,
   "事件版本" : 2.1 
  } 
 } 
}

透過採用這種方法,資料將專注於業務資訊,而後設資料則明確定義技術細節,如版本控制、事件型別和其他識別符號。這種分離提高了清晰度、可擴充套件性和可維護性。

關於事件版本
另外要強調一個重要的區別:不要混淆事件版本。

如果你將事件源ES納入到事件中,事件的版本與域事件的版本不同。

  • 一個標識合同的版本,
  • 而另一個管理併發性。

在事件溯源ES上下文中,事件的版本用於確保正確的處理順序。它有助於保持一致性並防止併發操作期間發生衝突。

相關文章