-
Notifications
You must be signed in to change notification settings - Fork 18
/
10-EventSourcing.fsx
297 lines (242 loc) · 9.45 KB
/
10-EventSourcing.fsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
(* ======================================
10-EventSourcing.fsx
Part of "Thirteen ways of looking at a turtle"
Related blog post: http://fsharpforfunandprofit.com/posts/13-ways-of-looking-at-a-turtle/
======================================
Way #10: Event sourcing -- Building state from a list of past events
In this design, the client sends a `Command` to a `CommandHandler`.
The CommandHandler converts that to a list of events and stores them in an `EventStore`.
In order to know how to process a Command, the CommandHandler builds the current state
from scratch using the past events associated with that particular turtle.
Neither the client nor the command handler needs to track state. Only the EventStore is mutable.
====================================== *)
#load "Common.fsx"
#load "FPTurtleLib.fsx"
open Common
open FPTurtleLib
// ======================================
// EventStore
// ======================================
type EventStore() =
// private mutable data
let eventDict = System.Collections.Generic.Dictionary<System.Guid,obj list>()
let saveEvent = new Event<System.Guid * obj>()
/// Triggered when something is saved
member this.SaveEvent =
saveEvent.Publish
/// save an event to storage
member this.Save(eventId,event) =
match eventDict.TryGetValue eventId with
| true,eventList ->
let newList = event :: eventList // store newest in front
eventDict.[eventId] <- newList
| false, _ ->
let newList = [event]
eventDict.[eventId] <- newList
saveEvent.Trigger(eventId,event)
/// get all events associated with the specified eventId
member this.Get<'a>(eventId) =
match eventDict.TryGetValue eventId with
| true,eventList ->
eventList
|> Seq.cast<'a> |> Seq.toList // convert to typed list
|> List.rev // reverse so that oldest events are first
| false, _ ->
[]
/// clear all events associated with the specified eventId
member this.Clear(eventId) =
eventDict.[eventId] <- []
// ====================================
// Common types for event sourcing
// ====================================
type TurtleId = System.Guid
/// A desired action on a turtlr
type TurtleCommandAction =
| Move of Distance
| Turn of Angle
| PenUp
| PenDown
| SetColor of PenColor
/// A command representing a desired action addressed to a specific turtle
type TurtleCommand = {
turtleId : TurtleId
action : TurtleCommandAction
}
/// An event representing a state change that happened
type StateChangedEvent =
| Moved of Distance
| Turned of Angle
| PenWentUp
| PenWentDown
| ColorChanged of PenColor
/// An event representing a move that happened
/// This can be easily translated into a line-drawing activity on a canvas
type MovedEvent = {
startPos : Position
endPos : Position
penColor : PenColor option
}
/// A union of all possible events
type TurtleEvent =
| StateChangedEvent of StateChangedEvent
| MovedEvent of MovedEvent
// ====================================
// CommandHandler
// ====================================
module CommandHandler =
/// Apply an event to the current state and return the new state of the turtle
let applyEvent log oldState event =
match event with
| Moved distance ->
Turtle.move log distance oldState
| Turned angle ->
Turtle.turn log angle oldState
| PenWentUp ->
Turtle.penUp log oldState
| PenWentDown ->
Turtle.penDown log oldState
| ColorChanged color ->
Turtle.setColor log color oldState
// Determine what events to generate, based on the command and the state.
let eventsFromCommand log command stateBeforeCommand =
// --------------------------
// create the StateChangedEvent from the TurtleCommand
let stateChangedEvent =
match command.action with
| Move dist -> Moved dist
| Turn angle -> Turned angle
| PenUp -> PenWentUp
| PenDown -> PenWentDown
| SetColor color -> ColorChanged color
// --------------------------
// calculate the current state from the new event
let stateAfterCommand =
applyEvent log stateBeforeCommand stateChangedEvent
// --------------------------
// create the MovedEvent
let startPos = stateBeforeCommand.position
let endPos = stateAfterCommand.position
let penColor =
if stateBeforeCommand.penState=Down then
Some stateBeforeCommand.color
else
None
let movedEvent = {
startPos = startPos
endPos = endPos
penColor = penColor
}
// --------------------------
// return the list of events
if startPos <> endPos then
// if the turtle has moved, return both the stateChangedEvent and the movedEvent
// lifted into the common TurtleEvent type
[ StateChangedEvent stateChangedEvent; MovedEvent movedEvent]
else
// if the turtle has not moved, return just the stateChangedEvent
[ StateChangedEvent stateChangedEvent]
/// The type representing a function that gets the StateChangedEvents for a turtle id
/// The oldest events are first
type GetStateChangedEventsForId =
TurtleId -> StateChangedEvent list
/// The type representing a function that saves a TurtleEvent
type SaveTurtleEvent =
TurtleId -> TurtleEvent -> unit
/// main function : process a command
let commandHandler
(log:string -> unit)
(getEvents:GetStateChangedEventsForId)
(saveEvent:SaveTurtleEvent)
(command:TurtleCommand) =
/// First load all the events from the event store
let eventHistory =
getEvents command.turtleId
/// Then, recreate the state before the command
let stateBeforeCommand =
let nolog = ignore // no logging when recreating state
eventHistory
|> List.fold (applyEvent nolog) Turtle.initialTurtleState
/// Construct the events from the command and the stateBeforeCommand
/// Do use the supplied logger for this bit
let events = eventsFromCommand log command stateBeforeCommand
// store the events in the event store
events |> List.iter (saveEvent command.turtleId)
// ====================================
// CommandHandlerClient
// ====================================
module CommandHandlerClient =
open CommandHandler
// filter to choose only StateChangedEvent from TurtleEvents
let stateChangedEventFilter = function
| StateChangedEvent ev -> Some ev
| _ -> None
/// create a command handler
let makeCommandHandler() =
let logger = printfn "%s"
let eventStore = EventStore()
let getStateChangedEvents id =
eventStore.Get<TurtleEvent>(id)
|> List.choose stateChangedEventFilter
let saveEvent id ev =
eventStore.Save(id,ev)
commandHandler logger getStateChangedEvents saveEvent
// Command versions of standard actions
let turtleId = System.Guid.NewGuid()
let move dist = {turtleId=turtleId; action=Move dist}
let turn angle = {turtleId=turtleId; action=Turn angle}
let penDown = {turtleId=turtleId; action=PenDown}
let penUp = {turtleId=turtleId; action=PenUp}
let setColor color = {turtleId=turtleId; action=SetColor color}
let drawTriangle() =
let handler = makeCommandHandler()
handler (move 100.0)
handler (turn 120.0<Degrees>)
handler (move 100.0)
handler (turn 120.0<Degrees>)
handler (move 100.0)
handler (turn 120.0<Degrees>)
let drawThreeLines() =
let handler = makeCommandHandler()
// draw black line
handler penDown
handler (setColor Black)
handler (move 100.0)
// move without drawing
handler (penUp)
handler (turn 90.0<Degrees>)
handler (move 100.0)
handler (turn 90.0<Degrees>)
// draw red line
handler penDown
handler (setColor Red)
handler (move 100.0)
// move without drawing
handler penUp
handler (turn 90.0<Degrees>)
handler (move 100.0)
handler (turn 90.0<Degrees>)
// back home at (0,0) with angle 0
// draw diagonal blue line
handler penDown
handler (setColor Blue)
handler (turn 45.0<Degrees>)
handler (move 100.0)
let drawPolygon n =
let angle = 180.0 - (360.0/float n)
let angleDegrees = angle * 1.0<Degrees>
let handler = makeCommandHandler()
// define a function that draws one side
let drawOneSide sideNumber =
handler (move 100.0)
handler (turn angleDegrees)
// repeat for all sides
for i in [1..n] do
drawOneSide i
// ======================================
// Tests
// ======================================
(*
CommandHandlerClient.drawTriangle()
CommandHandlerClient.drawThreeLines() // Doesn't go back home
CommandHandlerClient.drawPolygon 4
*)