Skip to content

Commit 39980de

Browse files
authored
docs: Updates based on DOs and DONTs (#429)
* update module Aggregate docs * Add Queries section * Add DDB article link * Add Light example * Remove Accumulator
1 parent 08382c7 commit 39980de

File tree

6 files changed

+97
-58
lines changed

6 files changed

+97
-58
lines changed

DOCUMENTATION.md

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -388,43 +388,63 @@ module Fold =
388388
389389
module Snapshot =
390390
391-
let generate (state: State): Event =
391+
let generate (state: State): Events.Event =
392392
Events.Snapshotted { ... }
393-
let isOrigin = function
394-
| Events.Snapshotted -> true
395-
| _ -> false
393+
let isOrigin = function Events.Snapshotted -> true | _ -> false
396394
let config = isOrigin, generate
397-
let hydrate (e: Snapshotted): State = ...
395+
let hydrate (e: Events.Snapshotted): State = ...
398396
399397
let private evolve state = function
400398
| Events.Snapshotted e -> Snapshot.hydrate e
401399
| Events.X -> (state update)
402400
| Events.Y -> (state update)
403401
let fold = Array.fold evolve
404402
405-
let interpretX ... (state: Fold.State): Events list = ...
403+
module Decisions =
406404
407-
type Decision =
408-
| Accepted
409-
| Failed of Reason
405+
let interpretX ... (state: Fold.State): Events.Event[] = ...
410406
411-
let decideY ... (state: Fold.State): Decision * Events list = ...
407+
type Decision =
408+
| Accepted
409+
| Failed of Reason
410+
411+
let decideY ... (state: Fold.State): Decision * Events.Event[] = ...
412412
```
413413

414414
- `interpret`, `decide` _and related input and output types / interfaces_ are
415415
public and top-level for use in unit tests (often unit tests will `open` the
416416
`module Fold` to use `initial` and `fold`)
417417

418+
In some cases, where surfacing the state in some way makes sense (it doesn't always; see [CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)), you'll have a:
419+
420+
```fsharp
421+
module Queries =
422+
423+
type XzyInfo = { ... }
424+
425+
let renderXyz (s: State): XzyInfo =
426+
{ ... }
427+
```
428+
429+
The above functions can all be unit tested directly. All other tests should use the `Service` with a `MemoryStore` via the `member`s on that:
430+
418431
```fsharp
419432
type Service internal (resolve: Id -> Equinox.Decider<Events.Event, Fold.State) = ...`
420433
421434
member _.Execute(id, command): Async<unit> =
422435
let decider = resolve id
423-
decider.Transact(interpretX command)
436+
decider.Transact(Decisions.interpretX command)
424437
425438
member _.Decide(id, inputs): Async<Decision> =
426439
let decider = resolve id
427-
decider.Transact(decideX inputs)
440+
decider.Transact(Decisions.decideX inputs)
441+
442+
member private _.Query(maxAge, render): Async<Queries.XyzInfo> =
443+
let decider = resolve id
444+
decider.Query(render, Equinox.LoadOption.AllowStale maxAge)
445+
446+
member x.ReadCachedXyz(id): Async<Queries.XyzInfo> =
447+
x.Query(TimeSpan.FromSeconds 10, Queries.renderXyz)
428448
429449
let create category = Service(Stream.id >> Equinox.Decider.forStream Serilog.Log.Logger category)
430450
```
@@ -1343,7 +1363,7 @@ which can then summarize the overall transaction.
13431363
### Idiomatic approach - composed method based on side-effect free functions
13441364

13451365
There's an example of such a case in the
1346-
[Cart's Domain Service](https://github.com/jet/equinox/blob/master/samples/Store/Domain/Cart.fs#L128):
1366+
[Cart's Domain Service](https://github.com/jet/equinox/blob/master/samples/Store/Domain/Cart.fs#L106):
13471367

13481368
```fsharp
13491369
let interpretMany fold interpreters (state: 'state): 'state * 'event[] =
@@ -1372,9 +1392,7 @@ _NOTE: This is an example of an alternate approach provided as a counterpoint -
13721392
there's no need to read it as the preceding approach is the recommended one is
13731393
advised as a default strategy to use_
13741394

1375-
As illustrated in [Cart's Domain
1376-
Service](https://github.com/jet/equinox/blob/master/samples/Store/Domain/Cart.fs#L99),
1377-
an alternate approach is to encapsulate the folding (Equinox in V1 exposed an
1395+
An alternate approach is to encapsulate the folding (Equinox in V1 exposed an
13781396
interface that encouraged such patterns; this was removed in two steps, as code
13791397
written using the idiomatic approach is [intrinsically simpler, even if it
13801398
seems not as Easy](https://www.infoq.com/presentations/Simple-Made-Easy/) at
@@ -2116,6 +2134,7 @@ Further information:
21162134
- [DynamoDB Transactions: Use Cases and Examples](https://www.alexdebrie.com/posts/dynamodb-transactions/) by Alex DeBrie
21172135
provides a thorough review of the `TransactWriteItems` facility (TL;DR: it's far more general than the stream level atomic
21182136
transactions afforded by CosmosDB's Stored Procedures)
2137+
- while it doesn't provide deeper insight into the API from a usage perspective, [Distributed Transactions at Scale in Amazon DynamoDB](https://www.infoq.com/articles/amazon-dynamodb-transactions) is a great deep dive into how the facility is implemented.
21192138

21202139
### Differences in read and resync paths
21212140

samples/Store/Domain.Tests/Domain.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<Compile Include="ContactPreferencesTests.fs" />
1111
<Compile Include="FavoritesTests.fs" />
1212
<Compile Include="SavedForLaterTests.fs" />
13+
<Compile Include="LightTests.fs" />
1314
</ItemGroup>
1415

1516
<ItemGroup>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Domain.Tests.LightTests
2+
3+
open Domain.Light
4+
open Swensen.Unquote
5+
6+
type Case = Off | On | After3Cycles
7+
let establish = function
8+
| Off -> initial
9+
| On -> fold initial [| SwitchedOn |]
10+
| After3Cycles -> [| for _ in 1..3 do SwitchedOn; SwitchedOff |] |> fold initial
11+
12+
let run cmd state =
13+
let events = decideSwitch cmd state
14+
events, fold state events
15+
16+
let [<FsCheck.Xunit.Property>] props case cmd =
17+
let state = establish case
18+
let events, state = run cmd state
19+
match case, cmd with
20+
| Off, true -> events =! [| SwitchedOn |]
21+
| Off, false -> events =! [||]
22+
| On, true -> events =! [||]
23+
| On, false -> events =! [| SwitchedOff |]
24+
| After3Cycles, true -> events =! [| Broke |]
25+
| After3Cycles, false -> events =! [||]
26+
27+
[||] =! decideSwitch cmd state // all commands are idempotent

samples/Store/Domain/Cart.fs

Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -103,40 +103,6 @@ let interpret command (state: Fold.State) =
103103
| SyncItem (Context c, skuId, None, w) ->
104104
yield! maybePropChanges c skuId w |]
105105

106-
#if ACCUMULATOR
107-
// This was once part of the core Equinox functionality, but was removed in https://github.com/jet/equinox/pull/184
108-
// it remains here solely to serve as an example; the PR details the considerations leading to this conclusion
109-
110-
/// Maintains a rolling folded State while Accumulating Events pended as part of a decision flow
111-
type Accumulator<'event, 'state>(fold: 'state -> 'event[] -> 'state, originState: 'state) =
112-
let accumulated = ResizeArray<'event>()
113-
114-
/// The Events that have thus far been pended via the `decide` functions `Execute`/`Decide`d during the course of this flow
115-
member _.Accumulated: 'event[] =
116-
accumulated.ToArray()
117-
118-
/// The current folded State, based on the Stream's `originState` + any events that have been Accumulated during the the decision flow
119-
member _.State: 'state =
120-
accumulated |> fold originState
121-
122-
/// Invoke a decision function, gathering the events (if any) that it decides are necessary into the `Accumulated` sequence
123-
member x.Transact(interpret: 'state -> 'event[]): unit =
124-
interpret x.State |> accumulated.AddRange
125-
/// Invoke an Async decision function, gathering the events (if any) that it decides are necessary into the `Accumulated` sequence
126-
member x.Transact(interpret: 'state -> Async<'event[]>): Async<unit> = async {
127-
let! events = interpret x.State
128-
accumulated.AddRange events }
129-
/// Invoke a decision function, while also propagating a result yielded as the fst of an (result, events) pair
130-
member x.Transact(decide: 'state -> 'result * 'event[]): 'result =
131-
let result, newEvents = decide x.State
132-
accumulated.AddRange newEvents
133-
result
134-
/// Invoke a decision function, while also propagating a result yielded as the fst of an (result, events) pair
135-
member x.Transact(decide: 'state -> Async<'result * 'event[]>): Async<'result> = async {
136-
let! result, newEvents = decide x.State
137-
accumulated.AddRange newEvents
138-
return result }
139-
#else
140106
let interpretMany fold interpreters (state: 'state): 'state * 'event[] =
141107
let mutable state = state
142108
let events = [|
@@ -145,21 +111,13 @@ let interpretMany fold interpreters (state: 'state): 'state * 'event[] =
145111
yield! events
146112
state <- fold state events |]
147113
state, events
148-
#endif
149114

150115
type Service internal (resolve: CartId -> Equinox.Decider<Events.Event, Fold.State>) =
151116

152117
member _.Run(cartId, optimistic, commands: Command seq, ?prepare): Async<Fold.State> =
153118
let interpret state = async {
154119
match prepare with None -> () | Some prep -> do! prep
155-
#if ACCUMULATOR
156-
let acc = Accumulator(Fold.fold, state)
157-
for cmd in commands do
158-
acc.Transact(interpret cmd)
159-
return acc.State, acc.Accumulated }
160-
#else
161120
return interpretMany Fold.fold (Seq.map interpret commands) state }
162-
#endif
163121
let decider = resolve cartId
164122
let opt = if optimistic then Equinox.LoadOption.AnyCachedValue else Equinox.LoadOption.RequireLoad
165123
decider.Transact(interpret, opt)

samples/Store/Domain/Domain.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<Compile Include="SavedForLater.fs" />
1313
<Compile Include="InventoryItem.fs" />
1414
<Compile Include="Retries.fs" />
15+
<Compile Include="Light.fs" />
1516
</ItemGroup>
1617

1718
<ItemGroup>

samples/Store/Domain/Light.fs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/// By Jérémie Chassaing / @thinkb4coding
2+
/// https://github.com/dddeu/dddeu20-talks-jeremie-chassaing-functional-event-sourcing/blob/main/EventSourcing.fsx#L52-L84
3+
module Domain.Light
4+
5+
type Event =
6+
| SwitchedOn
7+
| SwitchedOff
8+
| Broke
9+
type State =
10+
| Working of CurrentState
11+
| Broken
12+
and CurrentState = { on: bool; remainingUses: int }
13+
let initial = Working { on = false; remainingUses = 3 }
14+
let private evolve s e =
15+
match s with
16+
| Broken -> s
17+
| Working s ->
18+
match e with
19+
| SwitchedOn -> Working { on = true; remainingUses = s.remainingUses - 1 }
20+
| SwitchedOff -> Working { s with on = false }
21+
| Broke -> Broken
22+
let fold = Array.fold evolve
23+
24+
let decideSwitch (on: bool) s = [|
25+
match s with
26+
| Broken -> ()
27+
| Working { on = true } ->
28+
if not on then
29+
SwitchedOff
30+
| Working { on = false; remainingUses = r } ->
31+
if on then
32+
if r = 0 then Broke
33+
else SwitchedOn |]

0 commit comments

Comments
 (0)