This is part of a series of posts demonstrating software design patterns using C# and .NET. The patterns are taken from the book Design Patterns by the Gang of Four. Here's the series index page.

What Is This Pattern?

The State design pattern seeks to allow an object to change its own behavior when its internal state changes.

In this pattern, the "states" in which an object can exist are classes unto themselves, which refer back to the object instance and cause that instance's behaviors to differ depending on the state it is currently residing in.

The Rundown

  • Type: Behavioral
  • Useful? 3/5 (Sometimes)
  • Good For: Allowing an object's behavior to change as its internal state does.
  • Example Code: On GitHub

The Participants

  • The Context defines an interface of interest to the clients. It also maintains a reference to an instance of ConcreteState which represents the current state.
  • The State defines an interface for encapsulating the behavior of the object associated with a particular state.
  • The ConcreteState objects are subclasses which each implement a behavior (or set of behaviors) associated with a state of the Context.

A Delicious Example

Those of you who don't like red meat, turn back now. For everyone else, let's talk about steaks; specifically, how to cook them to different temperatures.

A set of four steaks on a grill Image is Grilling Steaks with border from Wikimedia, used under license.

The United States Food and Drug Administration sets guidelines as to how thoroughly cooked a steak must be in order to be a) safe to eat, and b) considered a certain level of "doneness" (which is rapidly becoming my new favorite word). For many steaks, the levels of doneness are:

  • Uncooked (not safe to eat)
  • Rare
  • Medium-Rare (mid-rare)
  • Medium
  • Medium-Well (mid-well)
  • Well done

Let's implement a system which keeps track of the internal temperature of a steak and assigns a level of doneness to it. We can model this using the State design pattern.

First, let's define our State participant, which represents a "doneness" level for a steak (and maintains a reference to an actual Steak instance).

/// <summary>
/// The State abstract class
/// </summary>
abstract class Doneness  
{
    protected Steak steak;
    protected double currentTemp;
    protected double lowerTemp;
    protected double upperTemp;
    protected bool canEat;

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

    public double CurrentTemp
    {
        get { return currentTemp; }
        set { currentTemp = value; }
    }

    public abstract void AddTemp(double temp);
    public abstract void RemoveTemp(double temp);
    public abstract void DonenessCheck();
}

The abstract methods AddTemp(), RemoveTemp(), and DonenessCheck() will need to be implemented by each of the states we can place the steak in.

Now that we have the State participant, let's define some ConcreteState objects. First, let's define a state for when the steak is uncooked and, therefore, not safe to eat. In this state, we can add cook temperature and remove cook temperature, but the steak will not be safe to eat until the cook temp is above 130 degrees fahrenheit (54.4 degrees Celsius).

We also need to implement the method DonenessCheck(), which determines whether or not the internal temperature of the steak is sufficiently high enough to allow it to move to another state. In this case, we'll make the assumption that a steak may only move to the next state of Rare.

/// <summary>
/// A Concrete State class.
/// </summary>
class Uncooked : Doneness  
{
    public Uncooked(Doneness state)
    {
        currentTemp = state.CurrentTemp;
        steak = state.Steak;
        Initialize();
    }

    private void Initialize()
    {
        lowerTemp = 0;
        upperTemp = 130;
        canEat = false;
    }

    public override void AddTemp(double amount)
    {
        currentTemp += amount;
        DonenessCheck();
    }

    public override void RemoveTemp(double amount)
    {
        currentTemp -= amount;
        DonenessCheck();
    }

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

Now let's think about the first edible state, Rare. In this state, we can add and remove cook temperature, and the steak is now edible (so we must initialize it as such).

/// <summary>
/// A 'ConcreteState' class.
/// </summary>
class Rare : Doneness  
{
    public Rare(Doneness state) : this(state.CurrentTemp, state.Steak)
    {
    }

    public Rare(double currentTemp, Steak steak)
    {
        this.currentTemp = currentTemp;
        this.steak = steak;
        canEat = true; //We can now eat the steak
        Initialize();
    }

    private void Initialize()
    {
        lowerTemp = 130;
        upperTemp = 139.999999999999;
        canEat = true;
    }

    public override void AddTemp(double amount)
    {
        currentTemp += amount;
        DonenessCheck();
    }

    public override void RemoveTemp(double amount)
    {
        currentTemp -= amount;
        DonenessCheck();
    }

    public override void DonenessCheck()
    {
        if (currentTemp < lowerTemp)
        {
            steak.State = new Uncooked(this);
        }
        else if (currentTemp > upperTemp)
        {
            steak.State = new MediumRare(this);
        }
    }
}

In a similar vein, we can implement the states for MediumRare, Medium, and WellDone.

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

    public MediumRare(double currentTemp, Steak steak)
    {
        this.currentTemp = currentTemp;
        this.steak = steak;
        canEat = true;
        Initialize();
    }

    private void Initialize()
    {
        lowerTemp = 140;
        upperTemp = 154.9999999999;
    }

    public override void AddTemp(double amount)
    {
        currentTemp += amount;
        DonenessCheck();
    }

    public override void RemoveTemp(double amount)
    {
        currentTemp -= amount;
        DonenessCheck();
    }

    public override void DonenessCheck()
    {
        if (currentTemp < 0.0)
        {
            steak.State = new Uncooked(this);
        }
        else if (currentTemp < lowerTemp)
        {
            steak.State = new Rare(this);
        }
        else if (currentTemp > upperTemp)
        {
            steak.State = new Medium(this);
        }
    }
}

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

    public Medium(double currentTemp, Steak steak)
    {
        this.currentTemp = currentTemp;
        this.steak = steak;
        canEat = true;
        Initialize();
    }

    private void Initialize()
    {
        lowerTemp = 155;
        upperTemp = 169.9999999999;
    }

    public override void AddTemp(double amount)
    {
        currentTemp += amount;
        DonenessCheck();
    }

    public override void RemoveTemp(double amount)
    {
        currentTemp -= amount;
        DonenessCheck();
    }

    public override void DonenessCheck()
    {
        if (currentTemp < 130)
        {
            steak.State = new Uncooked(this);
        }
        else if (currentTemp < lowerTemp)
        {
            steak.State = new MediumRare(this);
        }
        else if (currentTemp > upperTemp)
        {
            steak.State = new WellDone(this);
        }
    }
}

/// <summary>
/// A Concrete State class
/// </summary>
class WellDone : Doneness //aka Ruined  
{
    public WellDone(Doneness state) : this(state.CurrentTemp, state.Steak)
    {
    }

    public WellDone(double currentTemp, Steak steak)
    {
        this.currentTemp = currentTemp;
        this.steak = steak;
        canEat = true;
        Initialize();
    }

    private void Initialize()
    {
        lowerTemp = 170;
        upperTemp = 230;
    }

    public override void AddTemp(double amount)
    {
        currentTemp += amount;
        DonenessCheck();
    }

    public override void RemoveTemp(double amount)
    {
        currentTemp -= amount;
        DonenessCheck();
    }

    public override void DonenessCheck()
    {
        if (currentTemp < 0)
        {
            steak.State = new Uncooked(this);
        }
        else if (currentTemp < lowerTemp)
        {
            steak.State = new Medium(this);
        }
    }
}

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 add or remove temperature from the steak, it must call the current Doneness state's corresponding method.

/// <summary>
/// The Context class
/// </summary>
class Steak  
{
    private Doneness _state;
    private string _beefCut;

    public Steak(string beefCut)
    {
        _cook = beefCut;
        _state = new Rare(0.0, this);
    }

    public double CurrentTemp
    {
        get { return _state.CurrentTemp; }
    }

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

    public void AddTemp(double amount)
    {
        _state.AddTemp(amount);
        Console.WriteLine("Increased temperature by {0} degrees.", amount);
        Console.WriteLine(" Current temp is {0}", CurrentTemp);
        Console.WriteLine(" Status is {0}", State.GetType().Name);
        Console.WriteLine("");
    }

    public void RemoveTemp(double amount)
    {
        _state.RemoveTemp(amount);
        Console.WriteLine("Decreased temperature by {0} degrees.", amount);
        Console.WriteLine(" Current temp is {0}", CurrentTemp);
        Console.WriteLine(" Status is {0}", State.GetType().Name);
        Console.WriteLine("");
    }
}

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

static void Main(string[] args)  
{
    //Let's cook a steak!
    Steak account = new Steak("T-Bone");

    // Apply temperature changes
    account.AddTemp(120);
    account.AddTemp(15);
    account.AddTemp(15);
    account.RemoveTemp(10); //Yes I know cooking doesn't work this way, bear with me.
    account.RemoveTemp(15);
    account.AddTemp(20);
    account.AddTemp(20);
    account.AddTemp(20);

    Console.ReadKey();
}

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

The output of the sample application, showing how the Steak's state changes as its internal temperature does.

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

Will I Ever Use This Pattern?

Sometimes, more often if you deal with objects which change behaviors as their internal state changes. I personally have a lot of experience with this pattern, as I built a Workflow Engine database which is this pattern writ large and made changeable.

Summary

The State pattern allows the behavior of an object to change as its internal state changes, and it accomplishes this by making the states of an object separate classes from the object itself. Consequently, the states can implement their own behavior for the object, and the object can "react" to its internal state changing.

As always, I like to provide code with my tutorials, so the repository for this pattern is over on GitHub and contains all of the sample code used here.

Doneness, doneness done-NESS! Doneness, doneness, done-NESS! Seriously, that's a great word.

Happy Coding!

Get My eBook FREE!

Did you enjoy this post? This article and 21 others are also in my free eBook, "The Daily Design Pattern", which you can get just by signing up for my email list.