l

2022年7月11日 星期一

事件溯源(14):行為版本控制

July 05 21:59~23:12

▲圖1:行為無法控制

 

前言

上一集討論事件版本控制的議題,這一集要討論行為版本控制(Behavior Versioning)。這個問題Teddy也是讀了Gregory Young的《Versioning in an Event Sourced System》才發現,如果沒注意到程式的「行為版本」,在Event Sourcing系統中,可能會因為程式改版(領域事件不變)導致系統狀態錯誤。

 

***

問題範例

Gregory Young的在《Versioning in an Event Sourced System》書中有一個很簡單的例子來說明行為版本控制,在ezKanban中也有類似的例子但比較複雜一些,在這裡Teddy直接借用Gregory Young的例子。如圖2所示,銷售系統的POS Aggregate身上有sell()方法,它apply一個ItemSold領域事件,其中事件的最後一個參數是這筆銷售的金額小計。

 


▲圖2:sell method

 

圖3是ItemSold的event handler,它從領域事件拿出subTotal並將其乘上0.08以計算營業稅(tax是POS Aggregate身上的屬性);程式這樣寫看起來沒什麼問題。

 

▲圖3:計算稅金,1.0版程式

 

有一天政府把營業稅調高,從8%調整成10%,於是你把程式改成圖4。現在問題來了,當你重新載入POS之後,它會replay所有事件,然後假設原本有一個項目subTotal是100,tax採用1.0版程式計算出來的tax是8。但是程式改版之後,用2.0版程式計算出的tax變成了10。但是這筆交易產生的時間點,營業稅還是8%,所以不應該因為程式碼改變,就造成「竄改歷史」的情況。

 

▲圖4:計算稅金,2.0版程式

 

***


解決方法

如圖5所示,解決方法其實很簡單,就是把tax算好並當成領域事件內容的一部分,這樣就可以了。回到領域事件原始定義:「代表系統狀態改變」,所以領域事件內容應該「至少」要儲存「能夠代表本次狀態改變的所有資料。」在這個例子中,「稅率」是會隨著時間改變的數值,會影響tax的金額。因此ItemSold領域事件應該包含計算後的tax,或是只包含taxRate(稅率),之後再依據taxRate去計算tax也是可以。

 

▲圖5:修改後的版本,不會受到程式行為調整而改變聚合狀態

 

***

以上這個例子算是簡單的,在某些情況底下,如果Aggregate呼叫外部服務,也很可能會造成行為版本控制的問題,因而導致無法replay領域事件重現系統狀態。例如,向第三方金流API請款,如果replay領域事件會不會導致重複請款?這些都是要注意的細節。呼叫外部服務導致程式行為在replay變得不可決定(nondeterministic)的解決方法和上面計算稅金的例子類似,在產生領域事件時呼叫外部服務,並且把外部服務的回傳值儲存在領域事件上。如此可以達到確定性重播(deterministic replay)。總而言之,重點就是重建系統狀態所需的所有資料都要儲存在event stream裡面,如此每次replay才會出現相同的結果,如圖6所示。

 

▲圖6:將外部服務回傳結果存入領域事件中

 

Gregory Young在《Versioning in an Event Sourced System》書中還提到更多關於行為版本控制的細節,有興趣的鄉民請自行參考。

***

 

下集預告

在這一系列的文章中,Teddy使用EventStoreDB與PostgreSQL當作Event Store。早期採用Event Sourcing的開發人員有不少採用Apache Kafka當作Event Store。下一集要談是否合適把Apache Kafka當做Event Store?

***

友藏內心獨白:重建犯罪現場真的沒有那麼簡單啊。

沒有留言:

張貼留言