l

2022年6月29日 星期三

事件溯源(2):實作Event Sourced Aggregate

June 28 10:19~12:19


   ▲圖1:ezKanban簡化版的domain model

 

練習題目:Tag Aggregate

在領域驅動設計(Domain-Driven Design)中Aggregate(聚合)是一群物件的集合,它們的狀態改變必須在同一個交易中完成。也就是說Aggregate形成交易邊界,也是DDD中儲存狀態的最小物件單位。Aggregate的狀態交由Repository設計模式來儲存與讀取,一個Aggregate type對應到一個Repository

圖1是ezKanban系統core domain的領域模型,一共有四個aggregate,今天要用Event Sourcing的方式實作最簡單的Tag aggregate。

***

CreateTagUseCase Test

Teddy上【領域驅動設計與簡潔架構入門實作班】的時候採用TDD串起DDD、Event Storming與Clean Architecture的開發流程,要撰寫Tag aggregate依照慣例先幫它寫驗收測試案例,從CreateTagUseCaseTest開始。

圖2為CreateTagUseCase的驗收測試案例,Tag的屬性有tag id, board id, name, color這四個欄位。第37行執行完畢後一個新增的Tag會透過TagRepository被加入資料庫中。第29行透過CreateTagUseCase的建構函數將TagRepository注入給CreateTagUseCase。此時尚不需要決定TagRepository的實作細節,因此先用一個InMemoryTagRepository來「欺騙」CreateTagUseCase。

測試案例剛寫好的時候會有語法錯誤無法編譯,這是因為production code還沒寫,接下來撰寫CreateTagUseCase來消除語法錯誤。

 

▲圖2:CreateTagUseCase驗收測試案例 

***

CreateTagUseCase程式碼如圖3所示,很簡單就是新增一個Tag然後把它存入repository。同樣的,此時由於Tag尚未撰寫因此會有語法錯誤無法編譯程式。

 


▲圖3:CreateTagUseCase 

***

接著撰寫Tag單元測試,如圖4。

 

▲圖4:Tag單元測試 

 

最後撰寫Tag,圖圖5所示,它繼承ezKanban內建的AggregateRoot類別,第22行程式產生TagCreated領域事件代表Tag狀態改變。如果不管Event Sourcing,CreateTagUseCase使用案例就寫好了。但是因為Tag想要套用Event Sourcing,所以程式撰寫方式要加以修正。


▲圖5:沒有支援Event Sourcing的Tag aggregate 

 

***

Event Sourced Tag

要讓Aggregate支援Event Sourcing,只要把握以下原則即可:

首先,Aggregate的public Command(改變系統狀態的操作)只負責產生領域事件,然後呼叫定義在AggregateRoot身上的apply方法去套用這個領域事件,如圖6所示。

 

▲圖6:支援Event Sourcing的Tag aggregate建構函數 

 

其次,如圖7所示AggregateRoot的apply方法是一個Template Method,它依次呼叫ensureInvairant、when、ensureInvairant、addDomainEvent。在此先忽略ensureInvairant,接下來的when method是一個Template Method設計模式裡面的primitive operation, 子類別必須實作when用來撰寫事件處理程式(event handler)。addDomainEvnet則是將領域事件先保存在AggregateRoot身上,最後Repository可讀取AggregateRoot的領域事件來儲存狀態。

 

▲圖7:AggregateRoot的apply method 

 

最後,如圖8所示,Tag實作when方法來處理TagCreated領域事件。處理方式很簡單,只要把圖5中原本Tag建構函數的初始化程式碼移到when裡面即可。

 

▲圖8:Tag實作when方法 

***

實作rename方法

可以新增Tag之後,接下來看Tag的rename method,如圖9所示,首先判斷傳入的newName和Tag現有名字是否相等,如果是就直接離開(因為狀態沒有改變),若否則apply TagRenamed領域事件。

 

▲圖9:Tag的rename方法 

***

 

刪除Tag

傳統關聯式資料庫的刪除最簡單的做法就是把該筆資料直接從資料庫刪除(delete),但在Event Sourcing系統中基本上只會append事件不會把事件刪除,所以實作刪除的方式就是在AggregateRoot定義markAsDeleted方法,然後讓它的子類別去實作該方法。如圖10所示,Tag的markAsDeleted方法apply TagDeleted領域事件。Tag最後的when方法處哩TagCreated、TagRenamed、TagDeleted三個領域事件,如圖11所示。

 

▲圖10:Tag的rename方法 

 


▲圖11:Tag的when方法,處理三個領域事件 

 

***

從領域事件回復狀態

將Tag改成上述寫法之後,要透過領域事件回復狀態就非常簡單。請參考圖12單元測試案例,第69與71行分別新增TagCreated與TagRenamed領域事件,模擬從資料庫中讀取Tag的領域事件。第74行新增Tag,將剛剛產生的兩個領域事件傳給Tag建構函數。最後驗證tag的狀態是否正確。


▲圖12:Tag的when方法,處理三個領域事件 

 

如圖13所示,Tag建構函數接受一個,List<DomainEvent>,它直接呼叫super,也就是圖14。


▲圖13:Tag建構函數 

 

如圖14所示,AggregateRoot建構函數收到List<DomainEvent>之後,第38行跑一個for each逐一將領域事件傳給apply方法。也就是說「將事件重播(replay)以計算出目前狀態」


▲圖14:AggregateRoot建構函數 

***

下集預告

撰寫好支援事件溯源的Tag Aggregate之後,下集介紹如何將狀態儲存到EventStoreDB (https://eventstore.com) 資料庫中。

***

友藏內心獨白:是不是很簡單。

沒有留言:

張貼留言