Simple But Nice State Machine

As part of something that I was messing around with the other day I wanted to use a state machine, so decided to have a quick hunt around before reinventing the wheel. My search yielded a few interesting things, namely the following:

  • BBV Common : This is a pretty neat library for .NET which includes the following components to name a few
    • StateMachine
    • BootStrapper
    • EvaluationEngine
    • EventBroker
  • Stateless : This is a very easy to use state machine by Nicholas Blumhardt who is also responsible for AutoFac which is a very nice IOC Container. Nick also worked on MEF, so I think it is fair to say Nick knows his onions.

I opted for using Stateless for what I wanted to do, the documentation is pretty good and comes with a few samples. But for the sake of completeness lets go through a simple example now shall we.

Suppose we have a state machine that looks something like this

stateDemo

It is a simple state machine that actually models our JIRA work flow at work. So how might I implement that using Stateless? Well it is actually pretty easy, all we need to do is carry out  few steps

1. Create A State Machine

 StateMachine<state, Trigger> 

This allows us to create a StateMachine which will use a State object for its states, and a a Trigger for its triggers

The really nice thing about Stateless is that it allows you to create your own State types and trigger types. So we are free to create
our own state types, such as the following

public class State
{
    public string Name { get; private set; }

    ///
    /// Stateless has OnEntry/OnExit actions that can be run, but this just illustrates how you
    /// could go about creating your own states that run their own actions where good encapsulation is
    /// observed
    ///
    public Action OnEntryStateAction { get; private set; }

    ///
    /// Stateless has OnEntry/OnExit actions that can be run, but this just illustrates how you
    /// could go about creating your own states that run their own actions where good encapsulation is
    /// observed
    ///
    public Action OnExitStateAction { get; private set; }

    public State(string name, Action onEntryStateAction, Action onExitStateAction)
    {
        Name = name;
        OnEntryStateAction = onEntryStateAction;
        OnExitStateAction = onExitStateAction;
    }
}

2. Define some triggers

This can be any type you want, I opted for enum values, so for my example this look like this

 private enum Trigger { StartDevelopment, DevelopmentFinished, StartTest, TestPassed, TestFailed } 

3. Setup your statemachine

This is the major part of what you need to do, but it does boil down to a few simple steps

3.1 Initialise the statemachine

Ensure you state machine starts in the correct state

 jiraMachine = new StateMachine<state, Trigger>(states["ReadyForDevelopment"]); 

3.2 Configure all your states

Stateless provides an excellent API for setting up your states, and allows for many different configuration(s) to be expressed in code.

This is the biggest part of what you will need to do, in this step you configure the following elements of the state

  • OnEntry, which allows you to specify what you want done on entering of that state
  • OnExit, which allows you to specify what you want done on exiting of that state
  • Permit, which allows you to specify what triggers are permitted to allow this state to be transitioned to a new state
jiraMachine.Configure(states["ReadyForDevelopment"])
    .OnEntry(s => PrintStateOnEntry())
    .OnExit(s => PrintStateOnExit())
    .Permit(Trigger.StartDevelopment, states["InDevelopment"]);

3.3 Firing Triggers

Once you have your state machine setup you can simply fire triggers to transition from one state to another

Here is an example of that

Fire(jiraMachine, Trigger.StartDevelopment);

So I think that covers the basics, I think it’s time to see a fuller example, so lets look at a little sample. This sample follows the state machine diagram above. I have included one small imbellishment, which is a tiny bit of Reactive Extensions (Rx) to have a certain action performed (ok its a simple Console.Writeline but it could be anything you want) when you are within the “InDevelopment” state.

By using Rx we can simply Dispose of the IObservable subscription when we exit the state, which I think is nice

Anyway enough chat here is the full example

class Program
{
    private enum Trigger
    {
        StartDevelopment,
        DevelopmentFinished,
        StartTest,
        TestPassed,
        TestFailed
    }
    private Dictionary<string, State> states = new Dictionary<string, State>();
    private CompositeDisposable disposables = new CompositeDisposable();
    private StateMachine<State, Trigger> jiraMachine;

    //Toggle this to see the effect of states with multiple next states
    private bool simulateTestPassing = false;

    public Program()
    {
        states.Add("ReadyForDevelopment", new State("ReadyForDevelopment", null, null));
        states.Add("InDevelopment", new State("InDevelopment",
            (x) => Console.WriteLine(string.Format("Entered InDevelopment {0} State", x.Name)),
            (x) => Console.WriteLine(string.Format("Exited InDevelopment {0} State", x.Name))));
        states.Add("ReadyForTest", new State("ReadyForTest", null, null));
        states.Add("InTest", new State("InTest", null, null));
        states.Add("Closed", new State("Closed", null, null));
    }

    public void Run()
    {
        jiraMachine = new StateMachine<State, Trigger>(states["ReadyForDevelopment"]);

        jiraMachine.Configure(states["ReadyForDevelopment"])
            .OnEntry(s => PrintStateOnEntry())
            .OnExit(s => PrintStateOnExit())
            .Permit(Trigger.StartDevelopment, states["InDevelopment"]);

        jiraMachine.Configure(states["InDevelopment"])
            .OnEntry(s =>
                            {
                                disposables.Add(Observable.Interval(TimeSpan.FromSeconds(1))
                                .Subscribe(x => SendStyleCopNagEmail()));
                                jiraMachine.State.OnEntryStateAction(jiraMachine.State);
                            })
            .OnExit(s =>
                        {
                            disposables.Dispose();
                            jiraMachine.State.OnExitStateAction(jiraMachine.State);
                        })
            .Permit(Trigger.DevelopmentFinished, states["ReadyForTest"]);

        jiraMachine.Configure(states["ReadyForTest"])
            .OnEntry(s => PrintStateOnEntry())
            .OnExit(s => PrintStateOnExit())
            .Permit(Trigger.StartTest, states["InTest"]);

        jiraMachine.Configure(states["InTest"])
            .OnEntry(s => PrintStateOnEntry())
            .OnExit(s => PrintStateOnExit())
            .Permit(Trigger.TestFailed, states["InDevelopment"])
            .Permit(Trigger.TestPassed, states["Closed"]);

        jiraMachine.Configure(states["Closed"])
            .OnEntry(s => PrintStateOnEntry())
            .OnExit(s => PrintStateOnExit());

        Fire(jiraMachine, Trigger.StartDevelopment);

        Action completeTheRemainingStates = () =>
            {
                Fire(jiraMachine, Trigger.DevelopmentFinished);
                Fire(jiraMachine, Trigger.StartTest);
                if (simulateTestPassing)
                {
                    Fire(jiraMachine, Trigger.TestPassed);
                }
                else
                {
                    Fire(jiraMachine, Trigger.TestFailed);
                }
            };

        disposables.Add(Observable.Timer(TimeSpan.FromSeconds(5))
            .Subscribe(x =>
            {
                completeTheRemainingStates();
            }));

        Console.ReadKey(true);
    }

    static void SendStyleCopNagEmail()
    {
        Console.WriteLine("Don't forget to use StyleCop settings for any JIRA checkin");
    }

    static void Fire(StateMachine<State, Trigger> jiraMachine, Trigger trigger)
    {
        Console.WriteLine("[Firing:] {0}", trigger);
        jiraMachine.Fire(trigger);
    }

    void PrintStateOnEntry()
    {
        Console.WriteLine(string.Format("Entered state : {0}", jiraMachine.State.Name));
    }

    void PrintStateOnExit()
    {
        Console.WriteLine(string.Format("Exited state : {0}", jiraMachine.State.Name));
    }

    static void Main(string[] args)
    {
        Program p = new Program();
        p.Run();
    }
}

When you run this you should see some output something like this

[Firing:] StartDevelopment
Exited state : ReadyForDevelopment
Entered InDevelopment InDevelopment State
Don’t forget to use StyleCop settings for any JIRA checkin
Don’t forget to use StyleCop settings for any JIRA checkin
Don’t forget to use StyleCop settings for any JIRA checkin
Don’t forget to use StyleCop settings for any JIRA checkin
Don’t forget to use StyleCop settings for any JIRA checkin
[Firing:] DevelopmentFinished
Exited InDevelopment InDevelopment State
Entered state : ReadyForTest
[Firing:] StartTest
Exited state : ReadyForTest
Entered state : InTest
[Firing:] TestFailed
Exited state : InTest
Entered InDevelopment InDevelopment State

So I think you will agree Stateless is pretty cool, and well worth a look. As always here is a small demo app

StatelessDemo.zip

About these ads

6 thoughts on “Simple But Nice State Machine

  1. The simplicity and usability of Stateless is great. Inspired by it, we developed a workflow engine with a SQL back-end supporting storing configurations(state/action), transitions, mail templates, audit, escalations, user permissions and other features.

    • sachabarber says:

      Yeah Stateless is very cool, I have not looked at extending it yet, but its good to know its easy to do

  2. Glen Handke says:

    Hi Sasha,
    The images and links no longer work on this post. Would love to try out your demo. Thanks!
    Glen

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s