DDD 中的那些模式 — 領域事件

DDD 中的那些模式 — 領域事件

嚴格的說事件驅動並非一種模式,應該是一種架構風格或者編程範式。可是領域驅動設計中事件驅動所涵蓋的範圍沒有那麼大,每每只是做爲整個系統解決方案的一部分,因此我仍是把它歸類在模式的範疇內。java

事件不管對業務人員仍是開發者都是很是熟悉且容易理解的概念,所以不管是在平常的需求溝通,仍是系統設計中,事件都是創建領域模型時很是有用的工具。而在「事件風暴」這樣的分析方法中,「領域事件」更是不可或缺的元素。在繼續介紹領域驅動設計中的事件以前,咱們先了解一下爲何要使用「事件」模式。編程

爲何須要「領域事件」

在以前介紹 Aggregation 聚合的文章中曾談及,Aggregation 一個顯著的特色或是限制條件就是每一個事務應該只更新一個 Aggregation。無疑這對於系統設計提出了不小的挑戰,如何設計一個粒度適中,又能符合業務要求的 Aggregation 並非一件容易的事情。可是領域事件爲咱們提供了一種更爲優雅的解決方案,在 Aggregation 完成更新後產生一個新的事件並廣播出去,由其餘訂閱該事件的訂閱者完成其餘 Aggregation 的更新。這樣就解除了 Aggregation 之間的耦合。api

而另外一個能讓領域事件大顯身手的地方是不一樣限界上下文之間的交互。如今較爲流行的架構風格是將不一樣的限界上下文做爲不一樣的微服務,微服務之間經過 API 的形式交互。可是 API 並非惟一的解決方案,在某些場景下基於消息中間件的事件模型可以更好的下降耦合,提高系統的彈性。微信

如何使用「領域事件」

雖然事件的概念對於開發人員很好理解,可是在實際項目中真正使用事件驅動模式的卻不多。一部分緣由是事件驅動模式缺乏框架的支持,每每須要手工處理許多包括異常,順序發送等工做。另外一個緣由是事件驅動的編程模型與順序編程模型差別很大,在發出事件後就將程序的控制邏輯交給了事件的訂閱者,在開發與問題排查時不是那麼的直觀與方便。因此接下來的部分是我在一些事件驅動模式上的實踐經驗,但願能對你們有所幫助。數據結構

對領域事件建模

領域事件是一個對象,所以一樣須要建模,定義它的數據結構。在開始定義咱們的領域事件以前,仍是先介紹一下業務場景。架構

當一個保險理賠申請提交,經過一系列的流程審覈,肯定理賠金額等數據無誤後會有專人進行最後的二次審批,若是審批經過就能夠支付給保險受益人相關的費用。從業務上看,理賠審批經過後,會有一連串的後臺業務操做,首先是財務費用以及相關憑證的生產,而後是理賠通知書的生成與發送,若是保單因爲理賠終止則須要對保單進行進一步的操做。這些業務行爲無疑引入了數個 Aggregation 對象,確定沒法經過惟一的 Aggregation 在一個事務內完成,因此必須引入領域事件。如下是業務與事件的關係圖:app

Domain-event-cut.jpg

領域事件由 Aggregation 生成,在咱們的這個場景中,Aggregation 就是理賠案件對象 — ClaimCase。而領域事件的名稱的格式通常爲產生這個事件的 Aggregation 的名稱 + 產生事件的動詞的過去式,這裏產生事件的行爲是審批 — approve,因此咱們能夠把這個領域事件的名稱定爲: ClaimCaseApproved。這其實和事件風暴中的建議也同樣。框架

肯定了名字以後,咱們看一下事件內部的數據結構。ClaimCaseApproved 內部數據結構通常與產生它的 Aggragation 很類似,都是相對重要的領域對象數據,在咱們的業務場景中,會有理賠的案件號,保險單號,事故日期等。須要注意的是,對於領域事件,一般須要增長額外的兩個屬性,一個是事件的發生日期,還有一個是事件的惟一編號。這兩項對於問題的排查與調試,以及訂閱事件方的處理都是必需的。如下是事件的示例代碼:異步

public class ClaimCaseApproved {
  private String eventId;
  private LocalDateTime occuredOn;
  private long claimCaseId;
  private long policyId;
  private LocalDateTime accidentDate;
  ……
}

事件的生成,發送與訂閱

有了數據模型以後,咱們須要考慮的是在一個分層架構中,應該將事件相關的代碼放置於何處。至今爲止並無一個統一的規則,因此我介紹以前項目中曾經嘗試過的方法,其中有好的地方也有不方便的地方,具體選擇何種,就留給你本身了。ide

一種作法是在領域服務中處理事件發送與訂閱的邏輯,而事件的生成由領域對象,即 Aggregation 負責。咱們先看一下示例代碼:

public class ClaimCase {
  public ClaimCaseApproved approve() {
    ……
  }
}

這裏的代碼很簡單,ClaimCase 是一個 Aggregation 的領域對象,而 approve 方法執行的是審批的業務邏輯,它的返回結果就是它所產生的事件。接着看一下領域服務的代碼:

public class ClaimCaseService {
  private DomainEventPublisher publisher;
  ……
  public void approve() {
    ClaimCase claimCase = .....;
    ClaimCaseApproved claimCaseApproved = claimCase.approve();
    publisher.publish(claimCaseApproved);
    ……
  }
}

領域服務 ClaimCaseService 調用領域對象的 approve 方法得到生成的領域事件後進行發送,這裏的 DomainEventPublisher 只是一個接口,具體的實現會依賴與基礎設施層。這種作法的問題在於須要領域對象顯式的返回事件對象,若是你的領域對象的這個方法正好須要返回值,而 Java 又是一門不支持多個返回值的語言,那麼就有些尷尬了,比較直白的解決方案就是引入第三方庫,返回一個相似 Tuple 的數據結構。

還有一種事件生成的可選方案是在領域對象內部保留一個數據結構存儲產生的事件,而後在領域服務中調用特定的方法獲取已經產生的事件,再發送,示例的代碼以下:

public class ClaimCase implements DomainEventGenerator {
  private Map<DomainEventType, List<DomainEvent>> registeredDomainEvents;
  public void approve() {
      ……
      registerDomainEvent(new ClaimCaseApproved());
  }
}

此次 ClaimCase 方法再也不返回對應的領域事件,而是將事件保存在內部的 Map 中。接着看一下 ClaimCaseService 的變化:

public class ClaimCaseService {
  public void approve() {
        …
      claimCase.approve();
      Map<DomainEventType, List<DomainEvent>> registeredDomainEvents = claimCase.getRegisteredDomainEvents();
      publisher.publish(registeredDomainEvents);
    }
}

領域服務 ClaimCaseService 在調用了 claimCaseapprove 方法後,顯式的調用了 claimCase.getRegisteredDomainEvents 方法,獲取領域對象內部註冊的領域事件,而後再發送。

另外一種則是事件的發送,處理邏輯放在應用服務層,即 Application Service 中,具體的細節和領域服務中大同小異,我就不贅述了。可是有幾點是須要牢記的:

  1. 領域事件是領域邏輯的一部分,因此在領域層不該該依賴某些底層的框架或是中間件,例如直接依賴某個消息中間件的 api。
  2. 事件的發送應該是異步非阻塞的,不該該阻塞當前處理的線程。
  3. 設計上避免事件鏈的產生,即一個事件被處理後又產生了另外一個事件,第二個事件的處理又產生了第三個事件,在設計沒有注意的狀況會變成一個環。(別問我是怎麼知道的~~~)
  4. 考慮最終一致性的解決方案,記好日誌,以及事件丟失的處理與排查方案。

使用框架

不管上述何種方法你可能都須要經過「觀察者」這樣的模式實現事件驅動的整個架構,可是若是你是使用 Java 的,就可使用 Spring 這樣的框架,經過依賴注入將事件的訂閱,發佈從領域模型中剝離出去。下面讓咱們看看如何使用 Spring 實現以前的例子:

public class ClaimCaseApproved extends ApplicationEvent {
  ……
}

咱們的領域事件變化不大,只是繼承了由 Spring 提供的 ApplicationEvent 基類。 ClaimCaseService 中的 publisher 能夠經過 Spring 的注入 Spring 的 ApplicationPublisher

@Autowired
private ApplicationEventPublisher applicationEventPublisher;

Spring 中把 Subscriber 稱之爲 Listener,這裏咱們能夠定義本身的訂閱者:

@Component
public class FinanceFeeSubscriber {
  @Autowared
  private FinanceClaimFeeApplicationService financeClaimFeeApplicationService;

  @Async
  @EventListener
  public void handleClaimCaseApproved(ClaimCaseApproved event) {
      financeClaimFeeApplicationService.generateClaimFeeFor(…);
      ……
  }
}

上面的代碼中咱們藉助 Spring 的能力注入了費用模塊的應用層服務 FinanceClaimFeeApplicationService ,而後經過 @Async@EventListener 兩個 annotation 聲明瞭處理領域事件的異步方法,在方法中咱們調用了應用層服務完成了因爲理賠審批經過引發的費用相關處理邏輯。

這裏須要注意的是要將方法聲明爲異步,這樣處理事件的方法就會在一個獨立的線程中運行,不會阻塞發佈事件的線程。其次是這裏業務上對事件處理的順序沒有要求,所以能夠並行處理,按照上述的代碼,能夠再建立兩個訂閱者,負責理賠通知書生成和保單終止的業務處理,彼此沒有影響。

若是你不想使用 Spring 提供可以的事件機制,能夠考慮使用 Google Gauva 提供的 EventBus,它提供了相似的功能,使用起來也很是簡單。

最後要注意的是,不管使用 Spring 仍是 Guava,事件數據都是保存在內存中的,若是遇到服務重啓極可能就會丟失未處理的數據,所以在項目中必定要記錄日誌並想好如何處理事件丟失的問題,必要時須要手工觸發重發事件等機制。

小結

事件驅動是很是貼合人類思惟習慣的一種架構模式,而領域事件也是分析領域模型的優秀工具。雖然使用事件驅動的編程模型須要考慮一些額外的問題,例如線上的調試,事件的容錯,重發等,可是毋庸置疑的是領域事件爲咱們提供了更好的解除耦合的手段,可以將大量複雜的業務邏輯拆分到不一樣的事件訂閱者中處理,而彼此之間又保持着鬆耦合的關係。在項目容許的狀況下,我強烈推薦領域事件這種模式,有興趣的你不妨嘗試一下!

歡迎關注個人微信號「且把金針度與人」,獲取更多高質量文章

QR.png

相關文章
相關標籤/搜索