Using the State Pattern to Improve Branching

Posted on   /6 min read


When dealing with an object that has state, it's easy to get yourself into a tangle of if/else branches of logic. When I refer to "improving" in the title, what I suppose I'm really referring to is making your code more object oriented, and therefore ending up with easier to understand code. This isn't just about writing better code though, it's about having a better understanding of the functionality you are trying to achieve by writing the code.

I'm going to give a really simplified example using boiling a kettle as the example action, with cold, boiling and boiled as the 3 states our kettle could be in.

First of all, let's look at the example that would require complicated branching logic. Remember this example is very simplified so it may not seem so complicated here but in a less simplified context, it would be. These code samples are in C# but the principles here apply to any OO language.

Let's look at our "Branchy" kettle.

public class BranchyKettle
{
    public string Brand { get; set; }
    public KettleState State { get; set; }

    public BranchyKettle()
    {
        State = KettleState.Cold;
    }

    public void Boil()
    {
        if (State != KettleState.Boiling && State != KettleState.Boiled)
        {
            // Code here would start boiling of Kettle
            State = KettleState.Boiling;
        }
    }

    public void CheckForBoiled()
    {
        if (State == KettleState.Boiling)
        {
            // Kettle is boiling, is it finished?
            // Let's fake the kettle boiling...
            var random = new Random();
            if (random.Next(100) % 3 == 0)
            {
                State = KettleState.Boiled;
            }
        }
    }
}

public enum KettleState
{
    Cold,
    Boiling,
    Boiled
}

As you can see, in the method to Boil and CheckForBoiled, there are if statements to check the current state of the Kettle to determine if the action can be carried out.

Now let's look at the example that doesn't require this complicated branching logic.

Instead of using an enum to specify the state, we are going to make each state an object that implements an interface. Obviously what I'm about to do is overkill for this simplified example, I'm just trying to get the theory across.

So first let's see our new Kettle object.

public class Kettle
{
    public string Brand { get; set; }
    public IKettleState State { get; set; }

    public Kettle()
    {
        State = new Cold();
    }

    public void Boil()
    {
        State = State.Boil();
    }

    public void CheckForBoiled()
    {
        State = State.CheckForBoiled();
    }
}

As you can see, there's much less code in this one! Now when we call Boil and CheckForBoiled, we pass the responsibility of deciding what the Kettle can do onto the state object. First, let's have a quick look at the IKettleState interface.

public interface IKettleState
{
    IKettleState Boil();
    IKettleState CheckForBoiled();
}

IKettleState is a simple interface that just specifies that each state must handle the actions of Boil and CheckForBoiled. So let's have a look at the more interesting bit. How do each of the states implement this interface.

First up, the Cold state.

public class Cold : IKettleState
{
    public IKettleState Boil()
    {
        // Code here would start boiling of Kettle
        return new Boiling();
    }

    public IKettleState CheckForBoiled() => this; // Kettle isn't boiling, so can't be boiled
}

As you can see, this code is very easy to read! When you call Boil on a Kettle in the Cold state, it will start boiling and return Boiling as the new state. If you call CheckForBoiled it's just going to keep returning Cold, because it's never going to go straight from Cold -> Boiled without first Boiling.

Here is the Boiling state.

public class Boiling : IKettleState
{
    public IKettleState Boil() => this; // Kettle is already boiling, can't be boiled again

    public IKettleState CheckForBoiled()
    {
        // Let's fake the kettle boiling...
        var random = new Random();
        if (random.Next(100) % 3 == 0)
        {
            return new Boiled();
        }
        return this;
    }
}

As you can see in this one, we're faking the Kettle being boiled but when it is boiled, it will return the Boiled state. Let's look at Boiled.

public class Boiled : IKettleState
{
    public IKettleState Boil() => this; // Kettle is already boiled
    public IKettleState CheckForBoiled() => this; // Kettle is boiled
}

The Boiled state is the simplest implementation of the three because it's the end of the road really (if you forget about the fact the Kettle will gradually return to Cold anyway).

I know this example is very simplified, but my reason for writing this post is because I used this pattern when writing code to model a domain that had an appointment at its core. As I mapped out the planned flows for the appointment, it turned out that the appointment had 9 possible states. There were also rules about what path an appointment has to follow. For example, an appointment that has been accepted by Person A can not then be rejected by Person A, it must be cancelled. An appointment also can't be rejected if it's already been cancelled. These are just a couple of the rules required to ensure the flow of an appointment was correct. There are so many possible states and rules around the appointment, that the appointment object is covered by 73 unit tests to make sure all the outcomes are covered and handled correctly.

If I'd written the logic in this case like the first example above, there would have been a few downsides:

  • My code wouldn't have been as readable and easy to understand for other people
  • My code would have been difficult to maintain
  • My code would have been difficult to cover well with unit tests
  • If a new requirement came up, it would have required existing code to change
  • I would not have gained such a clear and thorough understanding of the logic surrounding appointments

It's important not to underestimate the last bullet point. It wasn't until I started writing my code in the structure of the second example that I really started to understand the flow of the appointment. As a result I now not only have clean, readable and easy to understand code, I was also able to create a very simple flow diagram to help the non-coders understand the process.

What do you think? Do you ever structure your code like this to avoid complicated branching logic? Would you?


Thanks for taking the time to read this post. If you want to get in touch, you can find me on Mastodon.

< Back to home