DEV Community

Cover image for State Pattern in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on

State Pattern in C#

The State is a behavioural design pattern that lets an object alter its behaviour when its internal state changes. For the system, it appears as if the object changed its class.

The State design pattern is one of the most useful patterns described by the Gang of Four. Games often depend on the State pattern because objects can change so frequently. Many other simulations, whether they are games or not, depend on the State pattern as well.

You can find the example code of this post, on GitHub

Conceptualizing the Problem

The State pattern is closely related to the Finite-State Machine.

Example of a Finite-State Machine

The idea is that, at any given time, there's a finite number of states in which a program can be in. Within every unique state, the application behaves differently, and the program can be switched from one to the other instantaneously. However, depending on the current state, the application can only switch to certain states. The switching rules, called transitions, are predetermined.

This approach is very common around the web. Imagine we have a Post class on a blogging website. A post can be in one of three states: Draft, Private Publish and Published. The Publish method of the document works a little differently in each state:

  • In the Draft state, it publishes the document with a secret URL.
  • In the Private Publish state, it makes the document public.
  • In the Published state, it doesn't do anything.

Possible states and transitions of a post object.

State machines are usually implemented with long chains of conditional statements that select the appropriate behaviour depending on the object's current state. Usually, this state is just a set of variables. Even if you haven't heard about finite states before, you've probably implemented a state at least once.

public class Post
{
    private string state;

    //...

    public void Publish()
    {
        switch(state)
        {
            "draft":
                state = "private publish"
                break;
            "private publish":
                state = "published"
                break;
            "public publish":
                // do nothing
                break;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The greatest weakness of a state machine based on conditionals reveals itself as soon as we start adding states and state-dependent behaviours. Most methods will contain humongous conditionals that pick the proper behaviour of a method according to the current state. This code is very difficult to maintain because any change to the transition logic might require changing conditionals in every method.

The problem only worsens with time. It's quite difficult to predict all possible states and transitions during the design phase. Hence, a lean state machine can grow into a bloated mess over time.

The State pattern suggests that we create new classes for all possible states of an object and extract all state-specific behaviours into these classes.

Instead of implementing all behaviours, the original object called context, stores a reference to one of the state objects that represents its current state, and delegates all the state-related work to that object.

The document delegates the work to a state object

To transition the context into another state, replace the active state object with another object that represents that new state. This is possible only if all state classes follow the same interface and the context itself works with these objects through that interface.

This structure may look similar to the Strategy pattern, but there's one key difference. In the State pattern, the particular states may be aware of each other and initiate transitions from one state to another, whereas strategies rarely know about each other.

Structuring the State Pattern

In its base implementation, the State pattern has four participants:

Class Diagram of the State Pattern

  • Context: The Context stores a reference to one of the concrete state objects and delegates to it all state-specific work. The context communicates with the state object via the state interface. The context exposes a setter method for passing it a new state object.
  • State: The State interface declares the state-specific methods. These methods should make sense for all concrete states because we don't want some of our states to have methods that are never called.
  • Concrete State: The Concrete States provide their implementations for the state-specific methods. To avoid duplication of similar code across multiple states, we may provide intermediate abstract classes encapsulating some common behaviour. State objects may store a backreference to the context object. Through this reference, the state can fetch any required info from the context object, and initiate state transitions. Both context and concrete states can set the next stage and perform the actual state transition by replacing the state object linked to the context.
  • Client: The Client can trigger state changes to the Context object.

To demonstrate how the State pattern works, we will implement a component which keeps track of the internal temperature of a steak and assesses a level of "doneness" to it.

First, let's define our State participant, which represents the "doneness" level of a steak.

using State.Context;

namespace State.State
{
    /// <summary>
    /// The State abstract class
    /// </summary>
    public abstract class Doneness
    {
        protected Steak steak;
        protected double currentTemperature;
        protected double lowerTemperature;
        protected double upperTemperature;
        protected bool isSafe;

        public Steak Steak
        {
            get { return steak; }
            set { steak = value; }
        }

        public double CurrentTemperature
        {
            get { return currentTemperature; }
            set { currentTemperature = value; }
        }

        public abstract void IncreaseTemperature(double degrees);
        public abstract void DecreaseTemperature(double degrees);
        public abstract void DonenessCheck();
    }
}
Enter fullscreen mode Exit fullscreen mode

We have declared the IncreaseTemperature(), DecreaseTemperature(), and the DonenessCheck() abstract methods. These methods will be implemented by each of the states we can place the steak.

Now that we have the State participant, we will define some ConcreteState objects. First, we will define a state for when the steak is uncooked, and therefore not safe to eat. In this state, we can increase or decrease the cooking temperature, but the steak will not be safe for consumption until the core temperature is greater than 48.9 degrees Celsius.

We will also implement the method DonenessCheck(), which determines whether or not the internal temperature of the steak is sufficiently high to allow it to move to another state. At this point, we'll assume that a steak can only move one state at a time.

namespace State.State
{
    /// <summary>
    /// A Concrete State class.
    /// </summary>
    public class Uncooked : Doneness
    {
        public Uncooked(Doneness state)
        {
            currentTemperature = state.CurrentTemperature;
            steak = state.Steak;

            lowerTemperature = 0;
            upperTemperature = 48.9;
            isSafe = false;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature > upperTemperature)
                steak.State = new Rare(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Similarly, we can implement the rest of the states of the steak.

using State.Context;

namespace State.State
{
    /// <summary>
    /// A Concrete State class.
    /// </summary>
    public class Rare : Doneness
    {
        public Rare(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Rare(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 49;
            upperTemperature = 54.4;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Uncooked(this);

            if (currentTemperature > upperTemperature)
                steak.State = new MediumRare(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using State.Context;

namespace State.State
{
    public class MediumRare : Doneness
    {
        public MediumRare(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public MediumRare(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 54.5;
            upperTemperature = 57.2;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Rare(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Medium(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using State.Context;

namespace State.State
{
    public class Medium : Doneness
    {
        public Medium(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Medium(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 57.3;
            upperTemperature = 62.8;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new MediumRare(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Well(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using State.Context;

namespace State.State
{
    public class Well : Doneness
    {
        public Well(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Well(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 62.9;
            upperTemperature = 68.3;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Medium(this);

            if (currentTemperature > upperTemperature)
                steak.State = new WellDone(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using State.Context;

namespace State.State
{
    public class WellDone : Doneness
    {
        public WellDone(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public WellDone(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 68.4;
            upperTemperature = 73.9;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new Well(this);

            if (currentTemperature > upperTemperature)
                steak.State = new Burnt(this);
        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using State.Context;

namespace State.State
{
    public class Burnt : Doneness
    {
        public Burnt(Doneness state) : this(state.CurrentTemperature, state.Steak) { }

        public Burnt(double currentTemperature, Steak steak)
        {
            this.currentTemperature = currentTemperature;
            this.steak = steak;
            isSafe = true;

            lowerTemperature = 74.0;
            upperTemperature = double.MaxValue;
        }

        public override void DecreaseTemperature(double degrees)
        {
            currentTemperature -= degrees;
            DonenessCheck();
        }

        public override void DonenessCheck()
        {
            if (currentTemperature < lowerTemperature)
                steak.State = new WellDone(this);

        }

        public override void IncreaseTemperature(double degrees)
        {
            currentTemperature += degrees;
            DonenessCheck();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now that we have all of our states defined, we can finally implement our Context participant. In this case, the Context is a Steak class which maintains a reference to the Doneness state it is currently in. Further, whenever we increase or decrease the temperature of the steak, it must call the current Doneness state's corresponding method.

using State.State;

namespace State.Context
{
    /// <summary>
    /// The Context class
    /// </summary>
    public class Steak
    {
        private Doneness state;
        private string cut;

        public Steak(string cut)
        {
            this.cut = cut;
            state = new Rare(0.0, this);
        }

        public double CurrentTemperature
        {
            get { return state.CurrentTemperature; }
        }

        public Doneness State
        {
            get { return state; }
            set { state = value; }
        }

        public void IncreaseTemperature(double degrees)
        {
            state.IncreaseTemperature(degrees);
            Console.WriteLine($"Increased degrees by {degrees} degrees");
            Console.WriteLine($"    Current degrees is {CurrentTemperature}");
            Console.WriteLine($"    Doneness is {State.GetType().Name}");
            Console.WriteLine("");
        }

        public void DecreaseTemperature(double degrees)
        {
            state.DecreaseTemperature(degrees);
            Console.WriteLine($"Decreased degrees by {degrees} degrees");
            Console.WriteLine($"    Current degrees is {CurrentTemperature}");
            Console.WriteLine($"    Doneness is {State.GetType().Name}");
            Console.WriteLine("");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Main() method, we can use these states by creating a Steak object and then changing its internal temperature:

using State.Context;

Steak steak = new Steak("T-Bone");

steak.IncreaseTemperature(48.9);
steak.IncreaseTemperature(10);
steak.IncreaseTemperature(5);
steak.DecreaseTemperature(15);
steak.DecreaseTemperature(5);
steak.IncreaseTemperature(10);
steak.IncreaseTemperature(20);
Enter fullscreen mode Exit fullscreen mode

As we change the temperature, we change the state of the Steak object. The output of this method looks like the following:

Output of the State Pattern example

As the Steak instance's internal temperature changes, the Doneness state in which it currently resides also changes, and consequently the apparent behaviour of that object shifts to whatever behaviour is defined by the current state.

Pros and Cons of State Pattern

✔ We can organize the code related to particular states into separate classes, thus satisfying the Single Responsibility Principle ❌Applying the pattern can be overkill if a state machine has only a few states or rarely changes.
✔ We can introduce new states without changing existing state classes or the context, thus satisfying the Open/Closed Principle
✔ We can simplify the code of the context by eliminating bulky state machine conditionals.

Relations with Other Patterns

  • The Bridge, the State and the Strategy patterns have very similar structures. Indeed, all of these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isn’t just a recipe for structuring our code in a specific way. It can also communicate to other developers the problem the pattern solves.
  • The State can be considered as an extension of the Strategy pattern. Both patterns are based on composition: they change the behaviour of the context by delegating some work to helper objects. Strategy makes these objects completely independent and unaware of each other. However, State doesn’t restrict dependencies between concrete states, letting them alter the state of the context at will.

Final Thoughts

In this article, we have discussed what is the State pattern, when to use it and what are the pros and cons of using this design pattern. We then examined how the State pattern relates to other classic design patterns.

It's worth noting that the State pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.

Latest comments (0)