Parameterised Triggers and Re-Entrant States in Stateless v2
Since working on a clinical outcomes review system a couple of years ago, I’ve been aware of the gap between simple hand-coded workflows and the full-blown workflow tools.
Stateless embodies the idea that a state machine can use closures to implement workflow without taking on persistence responsibilities. So long as all of the data used to drive the state machine is external to it, a regular ORM or other persistence mechanism can deal with identity, persistence and transactional issues. This substantially reduces the technical complexity of the framework.
As a ‘fun’ project, I haven’t done much other than maintain the site since the first version was finished. The second version extends the paradigm just a little, but in doing so makes a wider range of scenarios cleanly approachable.
To illustrate the new features I’ll draw on Scott Allen’s nifty bug tracker example for Windows Workflow.
The triggers that drive transitions in a state machine are approximately analogous to events or commands. Like events and commands, triggers can be associated with parameters.
- When a bug is assigned, the parameter may be the assignee;
- When an order is cancelled, the parameter may be the reason;
Modelling these details in Stateless, I adopted the some design goals and constraints:
- Retain the existing simple syntax for non-parameterised triggers;
- Execute parameter-driven logic during the transition, i.e. after the exit events for the last state have fired;
- Make it clear to the user exactly when parameter-driven logic will execute;
- Control interference between parameter-driven logic and guard conditions;
- Ensure parameter type-safety at compile time.
To associate parameters with a trigger, StateMachine provides the SetTriggerParameters() generic methods:
SetTriggerParameters() accepts one generic parameter for each of the trigger’s parameters, so in this case the Assign trigger is being associated with a single parameter of type string.
This method is called once, when the state machine is being configured.
The return value in this case is a TriggerWithParameters<string> object that lets us invoke the Fire() method in a strongly-typed way:
Now, all of this would be fairly pointless if Fire() was the only place that the parameters needed checking. However, the firing of the trigger is only half of the story.
When the machine transitions into the new state, logic based on the trigger parameters will need to run:
Putting everything together behind the facade of a domain object leads to a natural, understandable design:
A more complete example is in the Stateless Mercurial repository.
One alternative I considered but later discarded was to parameterise the states rather than the triggers, something that seems to be more established in traditional state machine models. After a good deal of unsuccessful experimentation I concluded that state parameterisation isn’t as useful as the trigger-based version.
Stateless encourages you to attach program logic to entry and exit events for the states in your state machine.
One of the edge cases that the bug tracker example reveals is re-initialisation of an already active state. When the bug is assigned, an email might be sent to the assignee. If Assign() is called again, to reassign the bug to someone else, it makes sense to re-initialise the Assigned state by executing the exit actions then executing the entry actions again.
To enable this behaviour for a state, the PermitReentry() method is supplied:
The previous version of Stateless considered a self-transition to be the same as an ignored trigger. Version 2 requires that self-transitions are either explicitly ignored, or configured as re-entrant.
These small additions were actually quite a challenge, but Stateless is still a very small library. One idea I’m toyed with is to use it as a ‘kernel’ for higher-level state machine functionality, e.g. an XML or DSL-driven framework. If you have ideas or requests, feel free to visit the UserVoice forum!