函式化事件溯源的決策者模式 - thinkbeforecoding

banq發表於2021-12-19

決策者模式是一種思考隨時間變化的系統的概念方式。應用層和域程式碼之間的概念介面。它具有在它們之間產生極低摩擦的優勢。
 

六邊形架構



              ┌───────────────────┐
              │  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)


 

結論
您可能已經注意到,我們為執行決策器而編寫的所有基礎設施程式碼與實際域程式碼完全無關。它可以執行一個簡單的遊戲或一個複雜的業務系統,它仍然會保持不變。
另一個有趣的點是決策程式可以以多種方式執行。純粹在記憶體中,將狀態儲存在資料庫或事件儲存中。不需要更改域程式碼。這表明對基礎設施的高度獨立性。
原文點選標題

相關文章