函式化事件溯源的決策者模式 - thinkbeforecoding
決策者模式是一種思考隨時間變化的系統的概念方式。應用層和域程式碼之間的概念介面。它具有在它們之間產生極低摩擦的優勢。
六邊形架構
┌───────────────────┐ │ Clock System │ │ │ │ Actions │ ▼ │ Outputs │ ┌─────────────┐ │ ────────────►│ Subsystem │───────────────► │ └─────────────┘ │ │ ▲ │ │ │ │ ▼ │ │ ┌─────────────┐ │ │ │ State │ │ │ └─────────────┘ │ └───────────────────┘ |
輸入介面可以被抽象化(HTTP API、訊息佇列、UI 事件、命令列引數、定時器或警報……)並且引數可以直接傳遞給子系統。輸出互動也可以被抽象並隱藏在介面後面(出站 HTTP 呼叫、訊息或通知傳送......)。儲存狀態的狀態也可以隱藏在介面後面,以保護子系統免受實現細節的影響。
子系統中的程式碼就只是由系統邏輯組成。我們通常所說的業務邏輯或域。我們稱其餘的程式碼為應用層。
這就是此描述了六邊形架構,其中應用程式核心域透過埠介面與其環境鬆散耦合,實現為技術基礎架構的介面卡。
。。。
決策者模式
決策者模式在六邊形基礎上進化為:
Command ┌──────────────┐ ─────────►│ │ Events State │ decide ├───────┐ ┌────────►│ │ │ │ └──────────────┘ │ │ ┌──────────────┐ Event │ │ State │ │◄──────┘ ├───◄─────┤ evolve │ State │ │ │◄──────┐ │ └──────────────┘ │ └────────────────────────────────┘ |
當系統最初啟動時,它處於初始狀態。目前,還沒有發生任何事情。
當第一個命令到達時,我們有一個命令和一個狀態,我們可以將它們傳遞給決定函式。它將命令的結果作為事件列表返回,即響應輸入命令而發生的事情。
如果輸出事件列表為空,則什麼也沒有發生,不需要計算新狀態,我們準備好下一個命令。出於至少兩個原因,這不應經常發生。一個幾乎什麼都不做的系統並不是很有趣,可能在沒有事件溯源的情況下實現……但是什麼都不做也會使某些情況更難診斷。當命令用於某事但未導致任何更改時,瞭解原因可能會很有趣。不發出事件將使基礎設施問題和主動決定不做任何事情之間的區別變得更難找到。是系統崩潰了,還是隻是決定什麼都不做?
仍然可以透過獲取為診斷而持久化的命令和可以從過去的事件中重建的狀態,將它們傳遞給決定函式並檢視結果,但它會要求仔細的程式碼分析或建立一個新的測試這個具體資料。這會佔用開發人員的時間,最好避免。
如果業務規則決定不做任何事情,建議將其明確化,並返回一個不會影響狀態的事件。它將清楚地出現在生成的事件中,並且診斷將很容易。這可以由無法訪問程式碼的支援團隊完成。
如果因為我們已經處於預期狀態而沒有發生任何事情,我們就處於冪等命令的情況。在這種情況下,實際上最好不返回 Events。它避免了無用的重複事件使事件儲存膨脹。
當 Event 列表包含一個 Event 時,我們可以呼叫帶有當前 State 和這個 Event 的進化函式。它將返回新狀態,用於下一個命令而不是初始狀態。
有時,事件列表將包含多個事件。在這種情況下,我們將為每個傳遞先前計算狀態的事件呼叫進化函式。
我們將迄今為止確定的七個元素的組合稱為決策器:
- 一個 Command 型別,表示可以提交給 Decider 的所有命令
- 一個 Event 型別,表示可以由 Decider 產生的所有事件
- 代表決策者所有可能狀態的狀態型別(可以只是所有事件的列表)
- 初始狀態是決策者在發生任何事情之前的狀態
- 一個決定函式,它接受一個命令和一個狀態並返回一個事件列表
- 一個進化函式,它接受一個狀態和一個事件並返回一個新的狀態
- 一個 isTerminal 函式,它接受一個狀態並返回一個布林值
在 F# 中,這可以建模為:
type Decider<'c,'e,'s> = { decide: 'c -> 's -> 'e list evolve: 's -> 'e -> 's initialState: 's isTerminal: 's -> bool } |
在記憶體中執行
我們可以輕鬆地讓決策程式在記憶體中的可變狀態變數上執行:
module InMemory = let start (decider: Decider<'c,'e,'s>) = let mutable state = decider.initialState fun (command: 'c) -> let events = decider.decide command state state <- List.fold decider.evolve state events events |
在資料庫上執行
之前的實現不是持久化的,一旦關閉就會失去任何狀態。我們可以像任何經典應用程式一樣在資料庫中持久化狀態:
module WithPersistence = let start (decider: Decider<'c,'e,'s>) = fun id (command: 'c) -> // load state from database let state = Storage.loadState(id) // this is the decision let events = decider.decide command state // compute new state let newState = List.fold decider.evolve state events // save state in database Storage.saveState(id, newState) events |
此版本很簡單,但如果狀態可以同時修改,則可能很危險。如果操作從不併發,則此程式碼很簡單而且非常好。
。。
在事件儲存區執行
它也可以使用事件儲存輕鬆實現:
module WithEventStore = let start (decider: Decider<'c,'e,'s>) = fun stream (command: 'c) -> // load all past events to compute current state let state = EventStore.loadEvents(stream, 0) |> List.fold decider.evolve decider.initialState // get events from the decision let events = decider.decide command state // append events to stream EventStore.appendEvents(stream, events) events |
此版本從每個命令的開頭重新載入所有事件。這對於短流是完全可以接受的。由於事件通常很小,載入少於 100 個事件非常快並且摺疊它們,幾乎是例項(將其視為執行一些基本操作的 100 次迭代的迴圈)。
快照
一旦您有很多事件,重新載入所有內容可能會變得很長。然後可以定期儲存狀態以及產生該狀態的流的版本。快照可以儲存在鍵值儲存中:
module WithSnapshots = let start (decider: Decider<'c,'e,'s>) = // load state using snapshot if any let loadState stream = // load snapshot let snapVersion, snapState = Snapshots.tryLoadSnapshot(stream) // fallback to version 0 and initialState if not found |> Option.defaultValue (0, decider.initialState) // load version and events after snapshot let version, events = EventStoreWithVersion.loadEvents(stream, snapVersion) // fold events after snapshot let state = List.fold decider.evolve snapState events version, state fun stream (command: 'c) -> let rec handle (version, state) = // get events from the decision let events = decider.decide command state // append events to stream match EventStoreWithVersion.tryAppendEvents(stream, version, events) with | Ok newVersion -> if isTimeToSnapshot version then // it is time to save snapshot // compute state let newState = List.fold decider.evolve state events // save it Snapshots.saveSnapshot(stream, newVersion, newState) events | Error(newVersion, newEvents) -> // there was a concurrent write // catchup missing events and retry let newState = List.fold decider.evolve state newEvents handle (newVersion, newState) // load all past events to compute current state // using snapshot if any let version, state = loadState stream handle (version, state) |
結論
您可能已經注意到,我們為執行決策器而編寫的所有基礎設施程式碼與實際域程式碼完全無關。它可以執行一個簡單的遊戲或一個複雜的業務系統,它仍然會保持不變。
另一個有趣的點是決策程式可以以多種方式執行。純粹在記憶體中,將狀態儲存在資料庫或事件儲存中。不需要更改域程式碼。這表明對基礎設施的高度獨立性。
原文點選標題
相關文章
- 無伺服器與事件溯源結合的演示案例:將事件溯源作為Azure函式的資料持久化機制的庫伺服器事件函式持久化
- 風控系統之事件溯源,決策流程記錄與版本控制事件
- 事件消費者之 Saga - 事件溯源事件
- 事件消費者之 Reactor - 事件溯源事件React
- 事件消費者之 Projector - 事件溯源事件Project
- 事件溯源與流水賬的結賬模式事件模式
- 拯救祭天的程式設計師——事件溯源模式程式設計師事件模式
- .NET分散式Orleans - 6 - 事件溯源分散式事件
- 使用函式式實現觀察者模式模式函式模式
- 事件溯源:投影或投射模式 -Kacper Gunia事件模式
- Java反應式事件溯源:領域Java事件
- 事件流與事件溯源事件
- 事件協作和事件溯源事件
- PHP 事件溯源PHP事件
- 事件溯源超越關聯式資料庫 - confluent事件資料庫
- Rust中的事件溯源 - ariseyhunRust事件
- 事件溯源模式:分離事件的發生和捕獲兩種不同時間 - verraes事件模式
- [靈性程式設計]函式委託,自動事件,函式觀察者(golang)程式設計函式事件Golang
- 剖玄析微聚合 - 事件溯源事件
- 事件溯源投影模式:重複資料刪除策略 - domaincentric事件模式AI
- Java反應式事件溯源之第5部分:事件儲存Java事件
- 系統記憶模式:事件溯源的力量,上下文為王! – thenewstack模式事件
- Chronicle事件溯源的最佳實踐事件
- Python的事件溯源開源庫Python事件
- 工廠模式的函式模式函式
- JavaScript中的事件迴圈機制跟函式柯里化JavaScript事件函式
- 事件溯源全指南 - Arkwrite事件
- 面對上層管理者的不合適的決策如何優化優化
- Java反應式事件溯源之第 2 部分:Actor 模型Java事件模型
- 事件溯源將顛覆關聯式資料庫! - Remy事件資料庫REM
- UNITY官方文件:事件函式Unity事件函式
- MYSQL事件使用 日期函式MySql事件函式
- 四年運維生產經驗分享:Nordstrom的事件溯源系列之二-生產者釋出模式運維事件模式
- .NET的事件溯源構建庫:Eventuous事件
- 用函式正規化實現戰略模式函式模式
- Java反應式事件溯源之第 4 部分:控制器Java事件
- 使用Kafka實現事件溯源Kafka事件
- 觀察者模式在One Order回撥函式中的應用模式函式