An illustration of how to employ State Machines for basic pattern recognition followed by a look at more advanced implementations such as data validation, the use of sub-states and the implementation of asynchronous action methods when transitioning into and out of a state.
Introduction
Complex systems can often be simplified by breaking them down into a series of discrete stages or states with transitions from state to state occurring as the system progresses. A state machine's function is to manage these transitions in response to some sort of input trigger. Ideally, the state machine should know very little about the states and their functionality, it just keeps a reference to the current state and directs all input towards that state. When a transition is made to another state, that state becomes the current state and inputs are directed towards it. The input triggers determine the next selected state, so, if modelling the production of a widget, the quality-control state could transition to the dispatch state or recycling state depending on the trigger received. To illustrate this, have a look at the UML State Machine Diagram of one of the purest forms of the state machine.
The Ancestral State Machine
This is the basic form of the state machine from which modern versions have evolved. There are only two triggers, a binary 1 or 0. The way to read the diagram is to start at the starting state and follow the transitions from state to state. State A is the starting state, the initial current state. It's marked by having an arrow pointing to it that doesn't come from another state. If this state receives a 0, there is no transition as indicated by the arrow pointing back towards its origin. A 1 causes the machine to make state B the current state. The inputs are now applied to state B where the response is different. A 0 trigger here causes a transition back to state A and a 1 causes a transition to state C. From state C, a 1 goes to state D and a 0 to state A. State D is the end state, it has no transitions to another state. End states are marked by two concentric circles, there can be more than one end state but there is only one start state. So what's the machine doing? It's detecting the pattern 111 within a string of binary inputs. If, at the end of the data stream, the current state is D the input is accepted, if not it's rejected. Notice that there's no memory in the system, the states are unaware of each other and all transitions are triggered by external inputs. The states have no intrinsic functionality; they only act as a target identifier for the triggers.
A Brief Introduction to Stateless
The following examples use the popular state machine Stateless that's available for download as a NuGet package. The machine itself is generic, you need to provide the type of the States and the type of the Triggers as well as the starting State when it is initialised. Simple applications usually define an enum
to represent the States and another enum
to represent the Triggers. The enum
s are then used as labels to relate to internally managed pseudo-states and triggers. This arrangement removes the need to define a common interface for all states - a difficult task at the best of times as the whole point of transitioning to a new state is to implement a different functionality.
Verifying an Email Address Using a Simple State Machine
This example shows how the basic state machine can be expanded to enable it to verify an email address. The context class is EmailValidator
, it encapsulates the state machine and takes a string inputted from an external source. The string is iterated over and its characters are used as triggers, the string is verified if the machine ends up in an acceptable state when the iteration has finished. The triggers are chars
and the States are an enum
.
public enum EmailState
{
Start,
Local,
Domain,
Accepted,
Rejected
}
public class EmailValidator : IValidator
{
private readonly StateMachine<EmailState, char> machine;
public EmailValidator()
{
machine = new StateMachine<EmailState, char>(EmailState.Start);
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
To simplify matters, all illegal characters are filtered out before triggering the machine so the machine can concentrate on determining if an email address is correctly formatted.
public bool Validate(string dataString)
{
char[] acceptable = new char[] { '@', '.', '-' };
if (dataString.Any(c => !char.IsLetterOrDigit(c) && !acceptable.Contains(c))
{
return false;
}
foreach (var c in dataString)
{
char trigger = char.IsLetterOrDigit(c) ? 'x' : c;
machine.Fire(trigger);
}
var isValid = machine.IsInState(EmailState.Accepted);
currentState = EmailState.Start;
return isValid;
}
The state machine diagram above shows how the machine is 'wired up'. The first character is inputted into the Start state. The characters @, dot and hyphen are rejected; all others are accepted and result in a transition to the Local state that deals with the local part of the email address. The Local state accepts all chars apart from @, @ causes a transition to the Domain state. The first character in the Domain state has to be alphanumeric. The Accepted state accepts everything apart from a hyphen and @.
Configuring the State Machine
States are configured by simply permitting the transitions that are allowed from each state using the Permit
method that takes a trigger and a state as parameters.
private void ConfigureMachine()
{
machine.Configure(EmailState.Start)
.Permit('@', EmailState.Rejected)
.Permit('.', EmailState.Rejected)
.Permit('x', EmailState.Local);
machine.Configure(EmailState.Local)
.Permit('@', EmailState.Domain);
.......
}
State Machine Diagrams
State Machine diagrams are a great debugging aid as they enable you to visualise the machine's configuration to the extent that someone with no prior knowledge of the configuration can see exactly how the machine is set up. Stateless has a method, UmlDotGraph.Format(machine.GetInfo())
, that outputs a Dot formatted string which, when pasted into the text box at Webgraphviz, will generate a graph. There is a free app available from the site that you can download to do the same thing.
Using States to Process Data
In the previous examples, the states are silent, they don't actually do any work. But, for states to be used in some form of production line, each state needs to be able to carry out work under the direction of the Context
class. The way this is achieved in Stateless is by the use of two Action
delegates, the OnEntry
and OnExit
Action delegates. OnEntry
is called when the state is transitioned into and the OnExit
method is called when the state is transitioned out of. It may be thought that the Action
delegates would not be very useful as they have no parameters and return void
. But that's not the case as the delegates are instantiated in the Context
and so they are able to capture all of the Context's public
and private
variables.
private readonly IValidator validator = new EmailValidator();
.....
machine.Configure(State.Validating)
.OnEntry(() =>
{
var address= Console.ReadLine();
Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
machine.Fire(trigger);
})
....
An important point is that the Validator
knows nothing about the State or the machine's OnEntry
method that's associated with the State. In order to maintain a good separation of concerns, all transitions should be handled by the Context
. It's not a good idea to allow any helper class such as the Validator
to become trigger happy and start firing on its own volition.
A Validation Example
This example mimics the some sort of validation process where the applicant has three chances to input a valid email address. After every unsuccessful attempt, the user has the option to cancel or retry. Three failed attempts result in the application being rejected. The Validating
state is configured to use the trigger Fail
as a guard trigger. Its transition depends upon the bool
returned from the IsRejected
method. If IsRejected
returns true
, the trigger Fail
causes a transition to the Rejected
end state. A false
value causes a transition to the Failed
state where there is an option to cancel or to try again.
machine.Configure(State.Validating)
.OnEntry(()
{
Tweet(Constants.StartValidating);
var address= Console.ReadLine();
Trigger trigger = validator.Validate(address) ? Trigger.Accept : Trigger.Fail;
machine.Fire(trigger);
})
.Permit(Trigger.Accept, State.Accepted)
.PermitIf(Trigger.Fail, State.Failed, () => !IsRejected)
.PermitIf(Trigger.Fail, State.Rejected, () => IsRejected);
Guard triggers are configured using the PermitIf
method. My preference is not to use them and to keep all the logic within the OnEntry Action
rather than let it escape into some sort of guided missile type trigger that hasn't got a predefined target.
Managing the Current State
In the validation example, the current state needs to be managed externally to the StateMachine
class so that it can be reset to the Start
state before each attempt at validation. Here's how to set it up, it's just a matter of providing a getter and setter as parameters to the constructor.
private EmailState currentState= EmailState.Start;
private readonly StateMachine<EmailState, char> machine;
public EmailValidator()
{
machine = new StateMachine<EmailState, char>(() => currentState, s => currentState = s);
machine.OnUnhandledTrigger((state, trigger) => { });
ConfigureMachine();
}
Substates
It's possible to designate a state as being a substate of some other state. The effect of this is that the superstate's OnExit
method is not called when the current state transitions from the superstate to the substate. In this example, SeatBelt
is a substate of Motoring
, Engine
is a substate of SeatBelt
and Brake
is a substate of Engine
. The substate status is inherited so SeatBelt
, Engine
and Brake
are all substates of Motoring
. When the Park
trigger is fired, OnExit
methods will be called consecutively, bubbling up from the Brake
state to the Motoring
state. Typically, there would just be the one external transition permitted into the Motoring
state and its substates but there can be multiple exit triggers.
private void ConfigureMachine()
{
machine.Configure(State.Start)
.Permit(Trigger.Motor, State.Motoring)
.OnEntry(() => Console.WriteLine("In State Start"))
.OnExit(() => Console.WriteLine("Leaving Start"));
machine.Configure(State.Motoring)
.Permit(Trigger.Fasten, State.Seatbelt)
.OnEntry(() => Console.WriteLine("Started Motoring"))
.OnExit(() => Console.WriteLine("Finished Motoring"));
machine.Configure(State.Seatbelt)
.SubstateOf(State.Motoring)
.Permit(Trigger.Engage, State.Engine)
.OnEntry(() => Console.WriteLine("Seatbelt Fastened"))
.OnExit(() => Console.WriteLine("Seatbelt Unfastened"));
machine.Configure(State.Engine)
.SubstateOf(State.Seatbelt)
.Permit(Trigger.Release, State.Brake)
.OnEntry(() => Console.WriteLine("Engine Started"))
.OnExit(() => Console.WriteLine("Engine Off"));
machine.Configure(State.Brake)
.SubstateOf(State.Engine)
.Permit(Trigger.Park, State.Parked)
.OnEntry(() => Console.WriteLine("Brake Released"))
.OnExit(() => Console.WriteLine("Brake Applied"));
machine.Configure(State.Parked)
.OnEntry(() => Console.WriteLine("Parked"));
}
Asynchronous OnExitAsync and OnEntryAsync Actions
The previous example could be refined by keeping the engine running and only stop it after the brakes have been applied. To do this, the Engine
state needs to remain active when it's no longer the current state. So there's a need to run the Engine's OnEntry Action
asynchronously and to end it when the state's OnExit
method is called. The way to achieve this is to use the OnExitAsync Func
and the FireAsync
method.
CancellationTokenSource cts = new CancellationTokenSource();
.....
machine.Configure(State.Engine)
.SubstateOf(State.Seatbelt)
.Permit(Trigger.Release, State.Brake)
.OnEntry( () =>
{
engineTask = Task.Run(()=>ChugChug(cts.Token));
Log($"Engine Started {engineNoise}");
})
.OnExitAsync(async() =>
{
cts.Cancel();
await engineTask;
Log("Engine Stopped");
});
The ChugChug
method is just a bit of nonsense that simulates the engine running.
private void ChugChug(CancellationToken token)
{
while (true)
{
Thread.Sleep(5);
if (token.IsCancellationRequested) break;
Console.ForegroundColor = ConsoleColor.White;
Console.Write(engineNoise);
}
}
When there is a transition from the Brake
state to the Parked
state, the OnExitAsync Func
of the current state is called first, then the call bubbles up through the substates to the Motoring
state. So the OnExitAsync Func
of all the Motoring
states except Engine
need to be configured in a similar way to this.
machine.Configure(State.Brake)
.SubstateOf(State.Engine)
.Permit(Trigger.Park, State.Parked)
.OnEntry(() => Log("Brake Released"))
.OnExitAsync(() =>
{
Log("BreakApplied");
return Task.CompletedTask;
});
The StartupAsync
method transitions the current state from the Start
state to the Parked
state.
public async Task StartupAsync()
{
machine.Fire(Trigger.Motor);
machine.Fire(Trigger.Fasten);
machine.Fire(Trigger.Engage);
machine.Fire(Trigger.Release);
string msg = machine.IsInState(State.Motoring) ? "is in " : "is not in ";
Log($"The current state is {machine.State}
it {msg}state Motoring",ConsoleColor.Yellow);
await Task.Delay(50);
Log("\r\nFiring Trigger Park",ConsoleColor.Yellow);
await machine.FireAsync(Trigger.Park);
}
The output from StartupAsync
is mainly frivolous but it gives an idea of how much functionality can be unleashed by simply firing a trigger.
Conclusion
State machines are useful for breaking the code up into a series of discrete sections and progressing the code from section to section. It’s true that configuring the machine needs some care and thought but, once set, it is unlikely to be corrupted by any other user-written code. The ability of state machines to produce a visual representation of the states and their transition triggers is a valuable asset as it provides a ‘wiring diagram’ of how the system is set up and greatly simplifies its maintenance and expansion. A state machine is not a panacea for all multiple pathway scenarios but it certainly knocks the IfThenElse pattern into a cocked hat.
History
- 6th May, 2020: Initial version