Last week I evaluated couple of State Machines. It wasn’t some structural evaluation, rather quick overview what is today in the market. I was just about to have something lightweight and easy to use so I have looked into Appccelerate and Stateless from Nicolas Blumhard. As I went through my sample I asked myself: Why we don’t use State Machines frequently? I asked couple of colleagues if they do. They don’t. Do I? No.

I have done some research and found some interesting blog posts and discussions by shopify, hacker news or this one written by Alan Skorkin. As Alan points: most of us have learned about State Machines at the University, as I did. And don’t forget, almost everyone is using one of them every day. You don’t have a idea where? Ask your coffee machine.

So state machines are neither unknown nor obsolete so I ask myself once again: why we don’t use them? As I was looking again I found blog post of Alex J. Champandard “10 Reasons the Age of Finite State Machines Over”. Although he discussed the usage of State Machines in context of game developing and the post is 8 years old, it is still interesting point of view from today’s perspective. I agree, maybe “they’re are unorthodox” but they also undervalued.

Sample

Look at this workflow featured as a finite state machine.

State Machine Credit Approval

How would I implement this workflow without any state machine framework? I asked two of my colleagues to do this and it was interesting to see what they have done: one has implemented the workflow by Observer Pattern the other created own state machine.

Stateless Sample

As counterexample I used Stateless and the experience was great. The definition was very straightforward.

public class CreditRequest
{
    public CreditRequest(double availableCredit, double requestedAmount)
    {
        AvailableCredit = availableCredit;
        RequestedAmount = requestedAmount;
    }

    public double AvailableCredit { get; }
    public double RequestedAmount { get; }
}

public class CreditRequestWorkflow
{
    public enum Event
    {
        Approve,
        Cancel,
        CheckIfAmountMustByApprovedByGroup,
        Decline,
        ReviewByCOO,
        ReviewByCFO
    }

    public enum State
    {
        Approved,
        CancelledByOriginator,
        Declined,
        NotStarted,
        WaitingOnGroupReview,
        WaitingOnCOOReview,
        WaitingOnCFOReview
    }

    private readonly CreditRequest _creditRequest;

    public CreditRequestWorkflow(CreditRequest creditRequest)
    {
        if (creditRequest == null)
        {
            throw new ArgumentNullException(nameof(creditRequest));
        }

        _creditRequest = creditRequest;

        StateMachine = new StateMachine<State, Event>(State.NotStarted);

        StateMachine.Configure(State.NotStarted)
                    .PermitIf(Event.CheckIfAmountMustByApprovedByGroup, State.WaitingOnGroupReview, () => _creditRequest.RequestedAmount > _creditRequest.AvailableCredit)
                    .PermitIf(Event.CheckIfAmountMustByApprovedByGroup, State.Approved, () => _creditRequest.RequestedAmount <= _creditRequest.AvailableCredit);

        StateMachine.Configure(State.WaitingOnGroupReview)
                    .SubstateOf(State.NotStarted)
                    .Permit(Event.ReviewByCOO, State.WaitingOnCOOReview)
                    .Permit(Event.Cancel, State.CancelledByOriginator);

        StateMachine.Configure(State.WaitingOnCOOReview)
                    .SubstateOf(State.WaitingOnGroupReview)
                    .Permit(Event.ReviewByCFO, State.WaitingOnCFOReview)
                    .Permit(Event.Decline, State.Declined);

        StateMachine.Configure(State.WaitingOnCFOReview)
                    .SubstateOf(State.WaitingOnCOOReview)
                    .Permit(Event.Approve, State.Approved)
                    .Permit(Event.Decline, State.Declined);

        StateMachine.Configure(State.Approved)
                    .SubstateOf(State.WaitingOnCFOReview)
                    .Ignore(Event.Decline)
                    .Ignore(Event.Cancel);

        StateMachine.Configure(State.Declined)
                    .SubstateOf(State.WaitingOnCOOReview)
                    .SubstateOf(State.WaitingOnCFOReview)
                    .Ignore(Event.Cancel)
                    .Ignore(Event.Approve);

        StateMachine.Configure(State.CancelledByOriginator)
                    .SubstateOf(State.WaitingOnCOOReview)
                    .SubstateOf(State.WaitingOnCFOReview)
                    .SubstateOf(State.WaitingOnGroupReview)
                    .Ignore(Event.Cancel)
                    .Ignore(Event.Approve);
    }

    public StateMachine<State, Event> StateMachine { get; }
}

Take we look at the usage. In this example I’m going through the workflow until the credit request will be approved.

[TestMethod]
public void AvailableCreditIsLessThanRequestedAmount_FireEventApprove_CurrentStateIsApproved()
{
    //Arrange
    var availableCredit = 50000D;
    var requestedAmount = 100000D;
    var creditRequest = new CreditRequest(availableCredit, requestedAmount);
    var workflow = new CreditRequestWorkflow(creditRequest);

    //Act
    workflow.StateMachine.Fire(Event.CheckIfAmountMustByApprovedByGroup);
    workflow.StateMachine.Fire(Event.ReviewByCOO);
    workflow.StateMachine.Fire(Event.ReviewByCFO);
    workflow.StateMachine.Fire(Event.Approve);

    var currentState = workflow.StateMachine.State;

    //Assert
    Assert.IsTrue(currentState == State.Approved);
}

I don’t know how you, but I think it’s elegant. Would you consider using State Machine next time? I will.