Evaluating the State Pattern

Recently, I’ve written a piece of code which is basically a state machine. I have written many state machines throughout my career and I’ve always followed the same method over and over again. However, in this particular situation, the code that I’ve written had to go through a cyclomatic complexity checker. (Ouch!) The state machine I designed didn’t meet the guideline. Therefore, I raised the question to more experienced Software Engineers and I got a suggestion to consider using the State Pattern, which is the topic of this article.

Surprisingly, I haven’t come across code written using the State Pattern in any of the projects that I have involved in. I also didn’t know about this pattern. Therefore, I decided to learn and apply the pattern on a hypotherical use case in order to evaluate the pros and cons of the implementation.

If you want to learn the pattern, I suggest reading this article.

Overview

This is how I’m going to structure the article.

  1. Explanation of a simplified state machine for a hypothetical smart washing machine
    • This state-machine will be used throughout the article
  2. Explanation of the traditional implementation of the state machine using C++
  3. Attempting the same implementation using the State Pattern
  4. Discussion on pros and cons

State machine for a hypothetical smart washing machine

DISCLAIMER:

I have never ever designed firmware for washing machines. I originally thought that I would use a scanner as my example, however, I could unintentionally disclose some interllectual property of some customers that I’ve worked for. Therefore, I decided to err on the side of caution and invent a dummy state machine for a hypothetical washing machine.

The state machine goes like

main_state_machine

To be honest, this is a very simple state machine. However, from experience I know that these state machines usually start small and then evolve into massive systems. Once they are massive, it is a nightmare to maintain them. My idea is to evaluate whether the State Pattern would provide any benefit in this regard.

Traditional state machine implementation

I’m going to use the following class hierarchy and develop some tests alongside. This will allow me to analyse the pattern in terms of testability as well. The main class that implements the logic is in the file washing_machine.cpp.

usual_implementation_classes

My first implementation can be downloaded from this github repository. The source code is inside 001_state_pattern subfolder. The most common implementation that I have seen of these state machines uses a massive switch case block.

void WashingMachine::Run()
{
    // Some common actions

    switch (m_state)
    {
        case WashingMachineState::IDLE:
            // Handle IDLE state
            break;

        case WashingMachineState::STANDBY:
            // Handle STANDBY state
            break;

        case WashingMachineState::STARTING_WATER:
            // Handle STARTING_WATER state
            break;

        case WashingMachineState::ADD_WATER:
            // Handle ADD_WATER state
            break;

        case WashingMachineState::STARTING_WASH:
            // Handle STARTING_WASH state
            break;

        case WashingMachineState::WASH:
            // Handle WASH state
            break;

        case WashingMachineState::STARTING_RINSE:
            // Handle STARTING_RINSE state
            break;

        case WashingMachineState::RINSE:
            // Handle RINSE state
            break;

        case WashingMachineState::STARTING_SPIN:
            // Handle STARTING_SPIN state
            break;

        case WashingMachineState::SPIN:
            // Handle SPIN state
            break;

        case WashingMachineState::DONE:
            // Handle DONE state
            break;

        case WashingMachineState::ERROR:
            // Handle ERROR state
            break;

        default:
            break;
    }
}

The main problem with this implementation is the fact that all the logic of the entire system (the system can be a component of a bigger product) is in one place. There would be conditionals everywhere which makes the program harder to follow. As I stated before, usually, these state machines evolve over time and become a giant bowl of spaghettie that is almost impossible to decipher.

Another issue that I see in this implementation is the transient states. These transients are always a cause of concern, because it is a very hard decision, design wise, to consider them a separate state or simply some action that needs to happen during a state change.

The State Pattern

Now let’s implement the same design using the state pattern to understand whether we can improve the design.

When I read the state pattern the main idea that I grasped is that it is centered around the fact that an entity that is modeled using a state machine (ie. the washing machine) can only be in one ‘state’ at any given time. The behaviour of the entity is completely different across states. Hence, we can model a state as a class independent of other classes.

Usually, there is one class called the context (In our case, I name it the WashingMachine class) contains a reference to the object representing the current state. Each of the states can talk to the context if it wants to change the state of the entity or if it wants to publish some data.

Here is the UML diagram for the design.

state_pattern_classes

When converting the previous implementation to the State Pattern implementation, I introduced changes incrementally in well defined small chunks. I introduced one state at a time. However, because of the way that I structured the code, I couldn’t tie the new implementation to the tests immediately. However, I always tried to make sure that the code can be compiled and all the previous tests can be executed.

I implemented the new state machine parallel to the old implementation. My plan was to swap the implementations once all the states have been introduced.

This is the recipe I followed to convert the implementation to the state pattern:

  1. Define the interface between WashingMachine (Context) and States.

    class IWashingMachineState
    {
    public:
        enum class State
        {
            // All states
        };
    
        virtual void Run() = 0;
        virtual void Reset() = 0;
        virtual State WhoAmI() = 0;
    
        virtual ~IWashingMachineState() {}
    };
    

    As you can see this is a very simple interface. Run() would execute the state, Reset() would carry our all actions that needs to be done in state transition and WhoAmI() is going to return what the current state is. If required, we can even have an Exit() function. (A better name for Reset() would be Enter() I guess)

  2. Define the interface for States to communicate with the Context. (State-to-Context interface)

    class IWashingMachineContext
    {
    public:
        virtual void ChangeState(IWashingMachineState::State state) = 0;
    
        virtual IWaterSensor::WaterLevel GetRecommendedWaterLevel(ILaundrySensor::LaundryLevel level) = 0;
        virtual void SetWaterLevelTarget(IWaterSensor::WaterLevel level) = 0;
        virtual IWaterSensor::WaterLevel GetWaterLevelTarget() const = 0;
        virtual IWashingMachineState::State GetPreviousState() const = 0;
    
        virtual ~IWashingMachineContext() {};
    };
    

    The only obvious function here is the ChangeState() function which is used by all the states to ask the Context to change the state. Every other function was introduced during the development as and when deemed required.

  3. Add an additional function Run2 to exercise the Context-to-State interface. The idea is to finally swap Run2 and Run without breaking any tests.

     void WashingMachine::Run2()
     {
         m_currentState->Run();
     }
    

    Compare this to the Run() function in the previous implementation. This just tells the current state to Run.

  4. Implement the States-to-Context interface for changing the state.

    void WashingMachine::ChangeState(StateName state)
    {
        m_currentState = m_states[state];
        m_currentState->Reset();
    }
    

    This code simply changes the state and calls the transient actions on the new state.

Now we have a prallel Run2 function in the code along with the required interfaces. Now we can switch one state at a time to new classes. Here’s how I did it,

  1. Create a class for the state and implement the interface (definition will be incomplete at this stage)
  2. Copy the corresponding case block to the Run() function of the new class
  3. Identify the dependencies of the state and inject them through the constructor of the State
  4. Implement the state control interface (Run, Enter, WhoAmI functions)
  5. Add the logic required to call ChangeState function
  6. In the Context class, initialize the newly created State class and store it in a container
  7. Repeat these steps for all the states

It’s hard to explain all the steps in detail here. Please have a look at the implementation of standby_state.cpp and washing_machine.cpp in the repository for a better understanding.

When performing the above steps, I came across situations where one state depends on the data generated by another state. In those cases, I added more functions to the State-to-Context interface to provide required communication.

There could be better ways to pass information among states other that introducing an additional function to the State-to-Context interface. It depends on how complex the interactions are. I feel that, if they can be conceptually seperated to another class, then by all means we should do that.

Finally, I swapped Run and Run2 functions and cleaned up the old code. None of the tests failed and none of the tests needed any change, which that proved that the functionality is still the same.

Tips

  1. Since this is a design change, it’s always a good idea to have unit tests to make sure that you leave no stone unturned.
  2. UML diagrams are your friends, use them.
  3. Always introduce the smallest change possible, compile and make sure that the structure of the code is intact.
  4. Better to have these three functions in any State. (Enter, Run, Exit)

Pros

  1. The states are focused and the implementation is less convoluted.
  2. The context class becomes very small and easily maintainable.
  3. Each state can be independently unit tested if required.
  4. Actions that happen on state boundaries can be clearly separated and implemented through Enter and Exit interfaces.

Cons

  1. An overkill for a small state machine.
  2. If too much data is passed across states, the interface between States-Context would be massive. However, this can be solved by introduce different abstractions for the data that is being communicated.

I hope this article was informative and if you have any questions, comments and objections please feel free to leave a comment. Remember I value every comment.

Happy coding!

-Pradeepa


comments powered by Disqus