The State design pattern in C#

A popular kata/interview question to have a crack at in your spare time is the vending machine. The vending machine has lots of different states and actions and requires a fair bit of logic to get it to behave how it should.

In this blog, I’m going to implement a dumbed-down version of a vending machine to hopefully unveil the important parts of the State design pattern without boring you too much.

The Kata

Let’s imagine we are designing one of those funky pizza vending machines that you might see in Japan.

We’ve been given a micro-controller and are expected to fulfil the following:

So the expected user journey would be for a customer to start at the No pizza selected state, they then select a pizza which takes them to the Awaiting payment state they then tap their card which takes them to the Pizza sold state, we then dispense the pizza, do a bit of logic to check we still have pizzas, if we do then we go back to the No pizza selected state else to the Out of pizzas state.

There are obviously a few flaws in this, and I’ve missed some actions for brevity. We will come back to this later in the blog to see how easy our code is to extend.

The first solution

In order to convert a state diagram to code we first need to get all the states β€” no pizza selected, awaiting payment, pizza sold, out of pizzas β€” and define some state variables, we then need to get all of the actions β€” select pizza, deselect pizza, taps card, dispense pizza β€” and perform some behaviour.


public class VendingMachine
{
    private const int NotSelected = 0;
    private const int AwaitingPayment = 1;
    private const int Sold = 2;
    private const int SoldOut = 3;
    
    private int _state;

    private int pizzas;
    
    public VendingMachine(int pizzas)
    {
        this.pizzas = pizzas;
        _state = NotSelected;
    }

    public void SelectPizza()
    {
        if (_state == NotSelected)
        {
            _state = AwaitingPayment;
            Console.WriteLine("You have selected a pizza, please tap your card!");
        } else if (_state == AwaitingPayment)
        {
            Console.WriteLine("You must first deselect your current pizza");
        } else if (_state == Sold)
        {
            Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
        } else if (_state == SoldOut)
        {
            Console.WriteLine("Sorry we're out of pizzas!");
        }
    }
    
    public void DeselectPizza()
    {
        if (_state == NotSelected)
        {
            Console.WriteLine("You haven't selected a pizza yet");
        } else if (_state == AwaitingPayment)
        {
            _state = NotSelected;
            Console.WriteLine("Deselected your pizza");
        } else if (_state == Sold)
        {
            Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
        } else if (_state == SoldOut)
        {
            Console.WriteLine("Sorry we're out of pizzas!");
        }
    }
    
    public void TapCard()
    {
        if (_state == NotSelected)
        {
            Console.WriteLine("You haven't selected a pizza yet");
        } else if (_state == AwaitingPayment)
        {
            _state = Sold;
            Console.WriteLine("Congratulations, you've bought a pizza!");
        } else if (_state == Sold)
        {
            Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
        } else if (_state == SoldOut)
        {
            Console.WriteLine("Sorry we're out of pizzas!");
        }
    }

    public void Dispense()
    {
        if (_state == NotSelected)
        {
            Console.WriteLine("You haven't selected a pizza yet");
        } else if (_state == AwaitingPayment)
        {
            Console.WriteLine("You need to pay before we dispense your pizza!");
        } else if (_state == Sold)
        {
            Console.WriteLine("Dispensing your pizza!");
            if (pizzas > 0)
            {
                _state = NotSelected;
            }
            else
            {
                _state = SoldOut;
            }

        } else if (_state == SoldOut)
        {
            Console.WriteLine("Sorry we're out of pizzas!");
        }
    }
}

Notice how we’ve set the state as instance variables and the actions determine the changing of the state. The dispense action is an internal process whereas all the other actions require the customer’s input.

So the above code seems to work as expected, the customer is happy, and they get their pizza.

Change request

Your pizza machine is situated in Tokyo next to another pizza machine ran by your rival company. Their machine seems far more popular than yours, queues of up to 20 people are a common occurrence. It seems your pizza machine is only getting used when people don’t have the time to wait for the rival pizza machine, that’s not good, what’s different? Free pizza.

Your rivals have come up with a brilliant idea to steal the market, there’s a 1 in 10 chance that the customer gets an extra pizza for free! We can’t compete with that, so let’s copy it.

We have a new state called free pizza which then dispenses two pizzas!

Lack of extensibility

Although our solution so far seemed pretty simple on the face of it, adding our new feature is going to be quite a pain. To add our new state we’re going to need to do the following:

  • introduce a new state instance variable
  • Add a new if statement to each one of our methods to handle the new state
  • Extra logic will be required in the TapCard method because you’ll need code to calculate whether the person is a winner and set them down the correct route if they are

This isn’t ideal, the code is already starting to look a bit spaghetti, if our rivals decide to add another feature that we need to copy then we’re going to need to hire a team of engineers just to understand our code. We need to refactor.

Refactoring

In order to refactor this we need to find what’s changing and encapsulate it. With the latest change request we’ve added a new Free pizza state, so let’s start by putting the states in their own classes and have the Vending machine delegate the work to the state classes. Hopefully, this will make adding new states less painful.

Let’s first do the refactor with the code we’ve got and then add the Free pizza state after to see if it’s easily extendable.

Introducing: The State design pattern:

The state pattern is a behavioral software design pattern that allows an object to alter its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The state pattern can be interpreted as a strategy pattern, which is able to switch a strategy through invocations of methods defined in the pattern’s interface.

https://en.wikipedia.org/wiki/State_pattern

This is how our refactor will look:

Let’s create the interface that all of the state classes will implement:

public interface IState
{
    public void SelectPizza();
    public void DeselectPizza();
    public void TapCard();
    public void Dispense();
}

And here’s an example class, notice how we’re passing in the vending machine object so that we can set the next state from within the states. Also notice how we no longer need a nasty nest of if statements:

public class NotSelectedState : IState
{
    private readonly VendingMachine _vendingMachine;

    public NotSelectedState(VendingMachine vendingMachine)
    {
        _vendingMachine = vendingMachine;
    }
    
    public void SelectPizza()
    {
        _vendingMachine.State = _vendingMachine.AwaitingPayment;
        Console.WriteLine("You have selected a pizza, please tap your card!");
    }

    public void DeselectPizza()
    {
        Console.WriteLine("You haven't selected a pizza yet");
    }

    public void TapCard()
    {
        Console.WriteLine("You haven't selected a pizza yet");
    }

    public void Dispense()
    {
        Console.WriteLine("You haven't selected a pizza yet");
    }
}

Here are the other states:

public class AwaitingPaymentState : IState
{
    private readonly VendingMachine _vendingMachine;

    public AwaitingPaymentState(VendingMachine vendingMachine)
    {
        _vendingMachine = vendingMachine;
    }
    
    public void SelectPizza()
    {
        Console.WriteLine("You must first deselect your current pizza");
    }

    public void DeselectPizza()
    {
        Console.WriteLine("Deselected your pizza");
    }

    public void TapCard()
    {
        _vendingMachine.State = _vendingMachine.Sold;
        Console.WriteLine("Congratulations, you've bought a pizza!");
    }

    public void Dispense()
    {
        Console.WriteLine("You need to pay before we dispense your pizza!");
    }
}
...
public class SoldState : IState
{
    private readonly VendingMachine _vendingMachine;

    public SoldState(VendingMachine vendingMachine)
    {
        _vendingMachine = vendingMachine;
    }
    
    public void SelectPizza()
    {
        Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
    }

    public void DeselectPizza()
    {
        Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
    }

    public void TapCard()
    {
        Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
    }

    public void Dispense()
    {
        Console.WriteLine("Dispensing your pizza!");
        _vendingMachine.Pizzas--;
        if (_vendingMachine.Pizzas > 0)
        {
            _vendingMachine.State = _vendingMachine.NotSelected;
        }
        else
        {
            _vendingMachine.State = _vendingMachine.SoldOut;
        }
    }
}
..
public class SoldOutState : IState
{
    private readonly VendingMachine _vendingMachine;

    public SoldOutState(VendingMachine vendingMachine)
    {
        _vendingMachine = vendingMachine;
    }
    
    public void SelectPizza()
    {
        Console.WriteLine("Sorry we're out of pizzas!");
    }

    public void DeselectPizza()
    {
        Console.WriteLine("Sorry we're out of pizzas!");
    }

    public void TapCard()
    {
        Console.WriteLine("Sorry we're out of pizzas!");
    }

    public void Dispense()
    {
        Console.WriteLine("Sorry we're out of pizzas!");
    }
}

And the vending machine class just proxies the requests to the current state:

public class VendingMachine
{
    public readonly IState NotSelected;
    public readonly IState AwaitingPayment;
    public readonly IState Sold;
    public readonly IState SoldOut;
    
    public IState State { get; set; }

    public int Pizzas { get; set; }
    
    public VendingMachine(int pizzas)
    {
        NotSelected = new NotSelectedState(this);
        AwaitingPayment = new AwaitingPaymentState(this);
        Sold = new SoldState(this);
        SoldOut = new SoldOutState(this);
        
        Pizzas = pizzas;
        State = NotSelected;
    }

    public void SelectPizza()
    {
        State.SelectPizza();
    }
    
    public void DeselectPizza()
    {
        State.DeselectPizza();
    }
    
    public void TapCard()
    {
        State.TapCard();
    }

    public void Dispense()
    {
        State.Dispense();
    }
}

That seems much better, we’ve saved ourselves about 20 if statements.

Implementing the feature request

Now we’ve done our refactor and implemented the State pattern, let’s go ahead and test out how easy it is to implement the new Free pizza state.

Let’s start off by creating the state itself:

public class FreePizzaState : IState
{
    private readonly VendingMachine _vendingMachine;

    public FreePizzaState(VendingMachine vendingMachine)
    {
        _vendingMachine = vendingMachine;
    }
    
    public void SelectPizza()
    {
        Console.WriteLine("You've already bought a pizza, and you've won a free one!");
    }

    public void DeselectPizza()
    {
        Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
    }

    public void TapCard()
    {
        Console.WriteLine("You've already bought a pizza, hold on whilst we dispense it!");
    }

    public void Dispense()
    {
        Console.WriteLine("Dispensing your two pizzas! πŸ•πŸ•");
        _vendingMachine.Pizzas-=2;
        if (_vendingMachine.Pizzas > 0)
        {
            _vendingMachine.State = _vendingMachine.NotSelected;
        }
        else
        {
            _vendingMachine.State = _vendingMachine.SoldOut;
        }
    }
}

This is very similar to the sold state except we’re dispensing two pizzas rather than one!

Next, we have to define the logic to determine whether the person has won or not. To do this we need to find the state that is before the sold state and the free pizza state, that’s the awaiting payment state. Let’s update that code:

    public void TapCard()
    {
        if (_vendingMachine.Pizzas >= 2)
        {
            var random = new Random();
            var randomNumber = random.NextInt64(10);

            if (randomNumber == 0)
            {
                Console.WriteLine("Congratulations, you've won an extra pizza!!");
                _vendingMachine.State = _vendingMachine.FreePizza;
                return;
            }
        }
        Console.WriteLine("Congratulations, you've bought a pizza!");
        _vendingMachine.State = _vendingMachine.Sold;    
    }

Which β€” for those of you playing at home β€” is setting the state to _vendingMachine.FreePizza which we need to add in the VendingMachine class:

    ...
    public VendingMachine(int pizzas)
    {
        ...
        FreePizza = new FreePizzaState(this);
       ...
    }

Happy customers

The state design pattern makes maintaining your finite state machine much cleaner, it reduces the cyclomatic and cognitive complexity making extending simpler. It follows the open/closed SOLID principle, meaning it is open for extension and closed for modification, i.e if your state machine requires a new state you don’t need to edit any existing states.

Our pizza vending machine now averages a queue roughly the same size as our competitors.

What states can you add to make our pizza the preferred choice of the punters?

If you’re interested in Design Patterns for C# I highly recommend the book Head First Design Patterns, it’s written in Java, but let’s face it, it’s the same thing. In fact, I’d highly recommend this book regardless of what OO language you write in.

All the source code for the blog can be found here.

And that’s pretty much all there is to it, if you liked this blog then please sign up for my newsletter and join an awesome community!

Leave a Reply