Skip to content

Commit

Permalink
refactor(Counter sample): Remove Command pattern (#465)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored Sep 6, 2024
1 parent 728695e commit 7d1e455
Showing 1 changed file with 41 additions and 38 deletions.
79 changes: 41 additions & 38 deletions samples/Tutorial/Counter.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,49 +37,51 @@ let [<Literal>] private CategoryName = "Counter"
// For this sample, we let callers just pass a string, and we trust it's suitable for use as a StreamId directly
let private streamId = FsCodec.StreamId.gen id

type State = State of int
let initial : State = State 0
(* Evolve takes the present state and one event and figures out the next state*)
let evolve state event =
match event, state with
| Incremented, State s -> State (s + 1)
| Decremented, State s -> State (s - 1)
| Cleared { value = x }, _ -> State x
(* NOTE the State is never stored directly, so it can be as simple and direct as necessary
Typically it's immutable, which enables it to be cached and/or safely have concurrent readers and writers etc *)
type State = int
let initial: State = 0
(* Evolve takes the present state and one event and figures out the next state
NOTE the logic should always be simple, with no decisions - it's just gathering/tracking facts that may be relevant to making a decision later
If you ever want to make it log or print outputs, that's a bad sign *)
let evolve state event: State =
match event with
| Incremented -> state + 1
| Decremented -> state - 1
| Cleared { value = x } -> x

(* Fold is folding the evolve function over all events to get the current state
It's equivalent to LINQ's Aggregate function *)
let fold state events = Array.fold evolve state events

(* Commands are the things we intend to happen, though they may not*)
type Command =
| Increment
| Decrement
| Clear of int

(* Decide consumes a command and the current state to decide what events actually happened.
This particular counter allows numbers from 0 to 100. *)

let decide command (State state) = [|
match command with
| Increment ->
if state < 100 then Incremented
| Decrement ->
if state > 0 then Decremented
| Clear i ->
if state <> i then Cleared {value = i } |]
let fold state = Array.fold evolve state

(* NOTE There's no Command DU (the history does show how it once worked using that)
Instead we have decision functions, and the Service passes one (together with any relevant inputs and helpers) to Decider.Transact
Each decision function gets to represent the outcome of the decision as zero, one or more events
One implication of that is that each decision can return a relevant result (though in many cases, returning unit is sufficient)
Equally importantly, for a real app, unit testing the decision logic is simple and direct, with extraneous boilerplate *)

let increment state = [| if state < 100 then Incremented |]
let decrement state = [| if state > 0 then Decremented |]
let reset value state = [| if state <> value then Cleared { value = value } |]

type Service internal (resolve: string -> Equinox.Decider<Event, State>) =

member _.Execute(instanceId, command) : Async<unit> =
member _.Decrement(instanceId) : Async<unit> =
let decider = resolve instanceId
decider.Transact(decide command)
decider.Transact decrement

member _.Increment(instanceId) : Async<unit> =
let decider = resolve instanceId
decider.Transact increment

member x.Reset(instanceId, value) : Async<unit> =
x.Execute(instanceId, Clear value)
let decider = resolve instanceId
decider.Transact(reset value)

member _.Read instanceId : Async<int> =
member _.Read instanceId: Async<int> =
let decider = resolve instanceId
decider.Query(fun (State value) -> value)
// id is the identity function, returning the full state. For anything real, you'd make probably project to a DTO
decider.Query id

(* Out of the box, logging is via Serilog (can be wired to anything imaginable).
We wire up logging for demo purposes using MemoryStore.VolatileStore's Committed event
Expand All @@ -100,9 +102,10 @@ let codec = FsCodec.Box.Codec.Create()
let cat = Equinox.MemoryStore.MemoryStoreCategory(store, CategoryName, codec, fold, initial)
let service = Service(streamId >> Equinox.Decider.forStream log cat)

let clientId = "ClientA"
service.Read(clientId) |> Async.RunSynchronously
service.Execute(clientId, Increment) |> Async.RunSynchronously
service.Read(clientId) |> Async.RunSynchronously
service.Reset(clientId, 5) |> Async.RunSynchronously
service.Read(clientId) |> Async.RunSynchronously
let instanceId = "ClientA"
service.Read(instanceId) |> Async.RunSynchronously
service.Increment(instanceId) |> Async.RunSynchronously
service.Decrement(instanceId) |> Async.RunSynchronously
service.Read(instanceId) |> Async.RunSynchronously
service.Reset(instanceId, 5) |> Async.RunSynchronously
service.Read(instanceId) |> Async.RunSynchronously

0 comments on commit 7d1e455

Please sign in to comment.