pwjazz/gflow
Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
--------------------------------- OVERVIEW ---------------------------------
Package gflow provides a mechanism for defining and processing
event-driven flows, which we'll just call 'flows'. These flows are
immutable and side-effect free, making them safe to use in multi-threaded
environments. Flows can be composed of other flows, making it easy to
create relatively complex flows from smaller constituent parts.
A flow is a directed acyclic graph of states, sharing the same start and end
states and connected by one or more transitions from one state to the next.
Each transition is governed by a single test, which is specified as a
function. These tests must be mutually exclusive.
---------------------------- STRUCTURE OF FLOWS ----------------------------
Let a, b, c ... equal a set of tests for advancing a flow
Let A, B, C ... equal a set of events that pass the respective tests
Let @ represent a state
Let --a-->, --b-->, ... represent transitions dependent on events a, b, etc.
Let A -> B -> C -> ... represent a sequence of events
For example, the below is a diagram of a 2-state flow with a single test 'a'
satisfied by a one-event sequence:
@ --a--> @
Satisfied by:
A
Multiple tests are composed into a flow using the following operators:
THEN a sequence of tests
OR logical OR (commutative)
AND logical AND (commutative)
The flow a THEN a THEN b is structured as follows:
@ --a--> @ --a--> @ --b--> @
Satisfied by:
A -> A -> B
Note: each event may trigger only 1 transition at a time, thus the above
flow is not satisfied by A -> B because A is not double counted.
Note: events that do not trigger a transition are effectively ignored, thus
the above flow is satisfied equally well by all three of the following:
A -> A -> B
A -> X -> A -> Y -> B
A -> A -> A -> A -> B
The flow c AND d is structured as follows:
|--c--> @ --d-->|
@ @
|--d--> @ --c-->|
Satisfied by:
C -> D
D -> C
The flow e OR f is structured as follows:
|--e-->|
@ @
|--f-->|
Satisfied by:
E
F
Flows can be composed by the same operators as tests, for example:
a THEN a THEN b OR (c AND d)
Satisfied by:
A -> A -> B
C -> D
D -> C
------------------------------- CODING ------------------------------------
// This example uses the flow a THEN a THEN b OR (c AND d)
// Define tests. Tests are functions that accept an EventData and
// return a bool. EventData is the empty interface, meaning it can be
// any type of value.
// In our example, we're just using strings as our EventData.
var a gflow.Test = func(data gflow.EventData) bool {
return "A" == data.(string)
}
var b gflow.Test = func(data gflow.EventData) bool {
return "B" == data.(string)
}
// ... and so on ...
// The flow is defined using the methods THEN OR and AND, which return
// state objects.
flow := a.THEN(a).THEN(b).OR(c.AND(d))
// Each of these compositional methods accepts two tests and returns a new
// state. Since states can be composed using these same methods, it is
// possible to break apart the definition of flows however is convenient.
subFlow1 := a.THEN(a).THEN(b)
subFlow2 := c.AND(d)
redefined := subFlow1.OR(subFlow2)
// the above redefined is equivalent to the original flow
// As mentioned previously, events can be anything, and in our case are just
// strings.
eventA := "A"
eventB := "B"
// To use a flow, call the Build() method and then Advance().
// Build() basically just returns the root state of the flow so that
// you start from the beginning. It also assigns ids to states, which is
// discussed further below.
state := flow.Build().Advance(eventA)
// Advance() returns the next state based on whatever transition fired (or
// did not fire). Continue advancing by calling Advance() on the returned
// states.
state = state.Advance(eventB)
state = state.Advance(eventC)
// ... and so on ...
// Above we assigned the result from Advance() to a variable named "state".
// In reality, state and flow are interchangeable - it's all just states.
// A flow can be referenced by any state in that flow, since they all
// share the same root.
state = state.Build().Advance(eventA).Advance(eventB)
// You can check whether a flow is finished by using the method Finished.
isFinished := state.Finished()
// Finished just means that there are no further transitions left.
// Once a flow is finished, it is legal to keep sending events to it,
// but this will have no effect.
// IMPORTANT - gflow states are immutable and their methods are free of
// side-effects, making them safe to use in a multi-threaded environment.
// For example, the below line creates a new flow from subFlow1 that
// requires the additional step f.
subFlow1 = subFlow1.THEN(f)
// Sub flow 1 is now @ --a--> @ -->a--> @ --b--> @ --f--> @
// Note how we assigned the result of THEN() to a variable. The original
// state referred to by subFlow1 before the assignment has not changed. So,
// any in-flight processes based on that flow can continue unhampered.
// The same property of thread-safety holds true for Advance().
state1 := flow.Advance(eventA)
state2 := flow.Advance(eventA)
// The above two states are completely independent of each other.
// The stateless nature of gflow means that the responsibility for managing
// the current state of flows is entirely the client program's.
// To help clients manage long running flows, gflow provides an ID property
// on states and a FindByID method to retrieve a state from a flow by its ID.
state = flow.Advance(eventA)
stateId = state.ID
// Save ID
// Do some other stuff
// Later ...
resumedState := flow.FindByID(stateId)
resumedState = resumedState.Advance(eventB)
// IMPORTANT: if the definition of a flow changes, even if it is logically
// equivalent to the previous definition, saved IDs will no longer be
// reliable. Therefore, once you have started using a flow, you should
// NEVER change its definition unless and until you no longer have any such
// flows in flight. For example:
flow1 := a.AND(b).OR(c.AND(d)).Build()
flow2 := c.AND(d).OR(a.AND(b)).Build()
s1 := flow1.Advance(eventA)
id := s1.ID
s2 := flow2.FindByID(id)
// Logically, these flows are the same, but the id's from these flows cannot
// be interchanged. In this case, s1 and s2 are NOT equivalent!