天天看點

領域服務、領域事件

綜合前兩篇總結,這篇對領域服務和領域事件做一個梳理。先說明下本文的領域服務和應用服務。SOA服務,或者應用間的RPC調用,Restful接口,或者通過消息中間件進行系統間的互動的,都可以歸類為應用服務。相較之下,領域服務不一定涉及到遠端調用或者重量級事務操作。是以上下文內建也就涉及到,怎樣的方式去劃分限界上下文,怎麼樣設計才能盡量減少應用服務的耦合,以及應用服務對于本地模型的防腐。

領域服務

概念

借用《Implementing Domain-driven Design》裡面的對于領域服務的定義。領域的某個操作過程或轉換過程不是實體或值對象的職責時,應該将操作放在一個單獨的接口中,即領域服務,并且要和通用語言保持一緻。這裡的非實體或值對象操作會有很多種情況,比如某個操作需要對多個領域對象操作,輸出一個值對象。在分層的架構中,有點類似于Manager。但是如果過渡抽象manager就會出現貧血,是以還需要確定領域服務是無狀态的,并且做好和貧血模型的權衡。可能大多數情況,領域服務的參數都是比實際的領域模型小的,隻有些關鍵屬性的值對象。如果服務隻操作領域的實體或值對象,則可以考慮下放到domain model中操作。

前面提到了Manager,但是很多應用中都會把Manager抽象成接口的形式,但大多數情況其實完全沒有必要,可以通過服務Factory的方式解耦,或者用Spring的@Service注解來注入真正的服務實作類。對于一些簡單的領域操作,還可以抽象一個迷你層,這個迷你層也可以稱作是領域服務,隻不過是無狀态,無事務,安全的一個抽象層。

領域事件其實也可以歸納為領域服務,不過領域服務的事件是幂等的。因為領域服務是無事務的,是以事件也是無副作用的,這樣在處理聚合依賴的時候,需要保證他們的最終一緻性。

領域事件

将領域中發生的活動模組化成一系列的離散事件,每個事件都用領域對象來表示。簡而言之,領域事件就是領域中發生的事件。還拿租書為例,一本書被借走了,那麼需要産生一個借書訂單,并且對于租書者來說,需要能檢視自己租書的清單和書籍詳情,同時這本書也需要被标記為不能再借出的狀态(因為已經被借走了)。這裡面bookRent就可以作為一個領域事件來發出。

事件的聚合

對于上述的事件模型,我們可以建立具有聚合特性的領域事件。這裡我們可以把這個事件本身模組化成一個聚合(BookRentEvent 對象),并且有自己的持久化方式。唯一辨別可以由一組屬性決定,在客戶方(Client)調用領域服務的時候建立這個領域事件{new bookRentEvent())},并添加到資源庫中,然後再通過消息的方式進行釋出。釋出成功後再回調更新時間狀态。但這裡需要注意,消息釋出最好和事件資源庫在相同的上下文,或共享資料源,這樣就可以保證事件的成功送出,在不同上下文系統,就需要做全局事務來保證。而唯一辨別在這裡的作用就是為了防止消息重發或者重複處理。是以訂閱方需要檢查重複消息,并且忽略。如果是本地上下文的事件,最好提供equals和hashcode 實作。

結合剛才的例子,在書籍管理上下文中,書被借走了,那麼書籍唯一表示和書的狀态(Rent被借出)就可以辨別一個事件。這個事件中需要有借書人的資訊(如id,nick等),那麼在持久化這個事件後,可以post一個Eventbus的本地消息,由使用者書籍領域服務監聽,更新使用者書籍清單等一系列操作。然後再Callback到事件源,更新事件狀态,處理成功。如果需要處理事件都在本地上下文,處理起來并不麻煩。

釋出領域事件

領域事件的釋出可以用Observer模式。在本地上下文,也要盡量減少對基礎設施或者消息中間件暴露領域模型,是以,需要将本地模型(領域模型)封裝成事件的聚合。比如我們不能直接釋出一個BookRent聚合的事件,而是一個BookRentEvent,這個Event對象,還會持有一些事件特有的屬性,比如可能根據需要,會有occurTime(發生時間),isConsumed(是否已經被處理)。事件釋出時,所有訂閱方都會同步收到通知。領域事件的主要元件就是publisher和subscriber了。

發送者

發送者本身并不表達一種領域概念,而是作為一種服務的形态。無論用什麼技術方式實作,用什麼架構,處理事件發送的思路也都可能不盡相同。比如,在web應用中,可以在啟動應用的時候處理訂閱者向發送者的事件注冊(避免注冊和處理發送的線程同步問題)。比如可以将關注的事件registe到本地的一個ThreadLocal的publisher List中。應用啟動完成後,開始處理領域事件的時候,就可以發送一個事件的聚合。這個事件的聚合是一個事件對象,而不是領域模型中的實體,因為我們要暴露需要暴露的事件給其他上下文,而不是暴露完整的領域對象。如果使用EventBus,我們可以在post的時候,封裝一個事件作為參數。

訂閱者

事件的訂閱者可以作為應用服務的一個獨立的元件。因為應用服務是在領域邏輯的外層,如果是純粹的事件驅動,那麼訂閱者作為一種應用服務,也可以定位成具有單一職責的,負責事件存儲的應用服務元件。

分布式領域事件

在處理分布式事件中,最重要也是最難處理的就是一緻性。消息的延遲,處理的不幂等就會影響領域模型狀态的準确性和事件的處理。但是我們在系統間互動的過程中,可以用一些技術方式來達到最終一緻性。這其中可能就需要進行事件模型的持久化。處理方式可以

1. 領域模型和消息設施共享持久存儲的資料源。這種需要事件作為一種本地事件模型存儲在和本地領域模型的同一個資料庫中。這樣保證了本地事務的一緻,性能較好,但是不能和其他上下文共享持久化存儲。

2. 全局XA事務(兩階段送出)來控制。模型和消息的持久化可以分開,但是全局事務性能差,成本高。

3. 在領域模型的持久化存儲中,單獨一塊存儲區域(單獨一張事件表)來存儲領域事件。也就是做本地的EventStore。但是需要有一個釋出事件的消息機制,消息事件是完全私有的。消息的發送可以交給消息中間件來處理。如果可以的話,還可以将時間存儲作為Rest資源。事件就可以以一種存檔日志的形式對外釋出事件(消息隊列,通過消息設施或者中間件發送RabitMQ,MetaQ等)。這樣還保證了時間的可追溯性。

我們使用事件來解耦,是為了考慮盡量避免RPC,簡化系統依賴,減少外部服務不可用對系統模型帶來的狀态影響。是以領域事件強調的是高度自治,但是也需要斟酌,通過事件處理的情況必須是容許延時的,并且消息的接收方需要是一個幂等接收器(可以自幂等,或者對于重複消息的拒絕處理),因為消息是可能重複發送的。

繼續閱讀