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.

4 comments

  1. Excellent work here – clear, concise with smart separation of your domain model and state machine. In your workflow class wouldn’t you want to have interface more like:

    workflow.CheckIfAmountMustByApprovedByGroup()

    workflow.Approve()

    to hide the details of the state machine? I don’t think I’d want those artifacts in my domain namespace.

    Thanks much Anton!

    Like

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 )

Facebook photo

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

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.