What is the State Design Pattern?

Göksu Deniz
8 min readDec 29, 2022

--

Image is created by DALL-E 2 AI

The State design pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. It appears as if the object has changed its class.

Imagine you have a robot that can do different things depending on how it’s feeling. Sometimes it might feel happy and want to dance, and other times it might feel sad and want to cry.

To make the robot easy to program, we can use something called the State design pattern. This is a way of organizing the robot’s code so that it’s easy to understand and change.

First, we create a list of all the different things the robot can do when it’s feeling different emotions. For example, when it’s feeling happy, it might want to dance, and when it’s feeling sad, it might want to cry.

Next, we create a special class for each emotion. Each class will contain a list of instructions for the robot to follow when it’s feeling that emotion.

Finally, we create a main class for the robot that will keep track of which emotion the robot is feeling. When the robot needs to do something, it will look at its current emotion and follow the instructions in the corresponding class.

This way, it’s easy to add new emotions and actions for the robot to do. All we have to do is create a new class with the instructions, and the robot will be able to use it automatically.

The State design pattern is a helpful way to organize code so that it’s easy to understand and change. It can be used in lots of different situations where an object needs to do different things depending on its current state.

Without the state design pattern, the code for the robot might look like this:

public class Robot
{
private string emotion;

public Robot()
{
emotion = "happy";
}

public void Dance()
{
if (emotion == "happy")
{
Console.WriteLine("Dancing happily!");
}
else
{
Console.WriteLine("Error: Can't dance while feeling sad.");
}
}

public void Cry()
{
if (emotion == "sad")
{
Console.WriteLine("Crying sadly.");
}
else
{
Console.WriteLine("Error: Can't cry while feeling happy.");
}
}

public void SetEmotion(string emotion)
{
this.emotion = emotion;
}
}

In this implementation, the Robot class maintains a field for its current emotion and uses conditional statements to determine the behavior of the Dance and Cry methods based on the current emotion.

To make the robot dance when it’s feeling happy, you could use code like this:

Robot robot = new Robot();
robot.Dance();

To make the robot cry when it’s feeling sad, you could use code like this:

Robot robot = new Robot();
robot.SetEmotion("sad");
robot.Cry();

This implementation has a number of problems:

  • The Dance and Cry methods are tightly coupled to the emotion field in the Robot class. This makes it difficult to add new emotions or change the behavior of the robot based on its emotion.
  • If we want to add a new emotion, such as “surprised,” we would have to modify the Robot class to include the new emotion and add additional conditional statements to the Dance and Cry methods to handle the new emotion.
  • The Robot class is responsible for both maintaining its emotion field and implementing the behavior associated with each emotion. This makes the class difficult to extend and maintain.

The State design pattern solves these problems by separating the responsibility for maintaining the internal state of an object (in this case, the emotion of the robot) from the responsibility for implementing the behavior associated with each state. This allows us to add new emotions and behaviors easily, and it also makes it easy to change the behavior of the robot at runtime by simply changing its emotion.

public interface IRobotState
{
void Dance();
void Cry();
}

public class HappyState : IRobotState
{
public void Dance()
{
Console.WriteLine("Dancing happily!");
}

public void Cry()
{
Console.WriteLine("Error: Can't cry while feeling happy.");
}
}

public class SadState : IRobotState
{
public void Dance()
{
Console.WriteLine("Error: Can't dance while feeling sad.");
}

public void Cry()
{
Console.WriteLine("Crying sadly.");
}
}

public class Robot
{
private IRobotState state;

public Robot()
{
state = new HappyState();
}

public void Dance()
{
state.Dance();
}

public void Cry()
{
state.Cry();
}

public void SetState(IRobotState state)
{
this.state = state;
}
}

In this example, the IRobotState interface defines the behavior that all states must implement. The HappyState and SadState classes are concrete implementations of this interface, and they define the specific behavior for each state.

The Robot class maintains a reference to the current state, and it delegates the Dance and Cry methods to the current state. The SetState method allows the Robot to change its state at runtime.

To make the robot dance when it’s feeling happy, you could use code like this:

Robot robot = new Robot();
robot.Dance();

To make the robot cry when it’s feeling sad, you could use code like this:

Robot robot = new Robot();
robot.SetState(new SadState());
robot.Cry();

The State design pattern can be useful in certain situations where an object’s behavior depends on its internal state and that state is likely to change at runtime.

Here are a few real-world scenarios that you can use the state design pattern:

  1. A media player that can play, pause, or stop audio or video files. The media player could use the State design pattern to change its behavior based on its current state (e.g. playing, paused, stopped).
  2. An ATM machine that can perform various actions such as withdrawing cash, checking account balance, or transferring funds. The ATM machine could use the State design pattern to change its behavior based on its current state (e.g. idle, withdrawing cash, checking balance).
  3. A traffic light that controls the flow of traffic at an intersection. The traffic light could use the State design pattern to change its behavior based on its current state (e.g. red, yellow, green).
  4. A text editor that allows users to edit, save, or print a document. The text editor could use the State design pattern to change its behavior based on its current state (e.g. editing, saving, printing).
  5. A game character that can move, attack, or defend itself. The game character could use the State design pattern to change its behavior based on its current state (e.g. moving, attacking, defending).

For an ATM scenario, we can use the state design pattern step by step like given below:

First we can create an interface for ATM state:

public interface IATMState
{
void InsertCard();
void EjectCard();
void EnterPin(int pin);
void WithdrawCash(int amount);
}

Then we need concrete classes which defined for states:

public class NoCardState : IATMState
{
private ATM atm;

public NoCardState(ATM atm)
{
this.atm = atm;
}

public void InsertCard()
{
Console.WriteLine("Card inserted.");
atm.SetState(new HasCardState(atm));
}

public void EjectCard()
{
Console.WriteLine("Error: No card to eject.");
}

public void EnterPin(int pin)
{
Console.WriteLine("Error: No card to enter PIN.");
}

public void WithdrawCash(int amount)
{
Console.WriteLine("Error: No card to withdraw cash.");
}
}

public class HasCardState : IATMState
{
private ATM atm;

public HasCardState(ATM atm)
{
this.atm = atm;
}

public void InsertCard()
{
Console.WriteLine("Error: Card already inserted.");
}

public void EjectCard()
{
Console.WriteLine("Card ejected.");
atm.SetState(new NoCardState(atm));
}

public void EnterPin(int pin)
{
if (pin == 1234)
{
Console.WriteLine("Correct PIN entered.");
atm.SetState(new HasCardCorrectPinState(atm));
}
else
{
Console.WriteLine("Incorrect PIN entered.");
atm.SetState(new HasCardIncorrectPinState(atm));
}
}

public void WithdrawCash(int amount)
{
Console.WriteLine("Error: Must enter PIN before withdrawing cash.");
}
}

public class HasCardCorrectPinState : IATMState
{
private ATM atm;

public HasCardCorrectPinState(ATM atm)
{
this.atm = atm;
}

public void InsertCard()
{
Console.WriteLine("Error: Card already inserted.");
}

public void EjectCard()
{
Console.WriteLine("Card ejected.");
atm.SetState(new NoCardState(atm));
}

public void EnterPin(int pin)
{
Console.WriteLine("Error: PIN already entered.");
}

public void WithdrawCash(int amount)
{
if (amount > 500)
{
Console.WriteLine("Error: Cannot withdraw more than $500 at a time.");
}
else
{
Console.WriteLine($"{amount} withdrawn.");
atm.Cash -= amount;
if (atm.Cash <= 0)
{
atm.SetState(new NoCashState(atm));
}
}
}
}

public class HasCardIncorrectPinState : IATMState
{
private ATM atm;

public HasCardIncorrectPinState(ATM atm)
{
this.atm = atm;
}

public void InsertCard()
{
Console.WriteLine("Error: Card already inserted.");
}

public void EjectCard()
{
Console.WriteLine("Card ejected.");
atm.SetState(new NoCardState(atm));
}

public void EnterPin(int pin)
{
Console.WriteLine("Error: Incorrect PIN entered.");
}

public void WithdrawCash(int amount)
{
Console.WriteLine("Error: Must enter correct PIN before withdrawing cash.");
}
}

public class NoCashState : IATMState
{
private ATM atm;

public NoCashState(ATM atm)
{
this.atm = atm;
}

public void InsertCard()
{
Console.WriteLine("Error: ATM is out of cash.");
}

public void EjectCard()
{
Console.WriteLine("Error: ATM is out of cash.");
}

public void EnterPin(int pin)
{
Console.WriteLine("Error: ATM is out of cash.");
}

public void WithdrawCash(int amount)
{
Console.WriteLine("Error: ATM is out of cash.");
}
}

And now we can create our ATM class:

public class ATM
{
public IATMState state;
public int Cash { get; set; }

public ATM()
{
state = new NoCardState(this);
Cash = 500;
}

public void SetState(IATMState state)
{
this.state = state;
}

public void InsertCard()
{
state.InsertCard();
}

public void EjectCard()
{
state.EjectCard();
}

public void EnterPin(int pin)
{
state.EnterPin(pin);
}

public void WithdrawCash(int amount)
{
state.WithdrawCash(amount);
}
}

In this example, the IATMState interface defines the behavior that all states must implement. The NoCardState, HasCardState, HasCardCorrectPinState, HasCardIncorrectPinState, and NoCashState classes are concrete implementations of this interface, and they define the specific behavior for each state.

The ATM class maintains a reference to the current state, and it delegates the InsertCard, EjectCard, EnterPin, and WithdrawCash methods to the current state. The SetState method allows the ATM to change its state at runtime.

To use the ATM, you can call the various methods on the ATM object to simulate different actions. For example, to insert a card and withdraw $100, you could use code like this:

ATM atm = new ATM();
atm.InsertCard();
atm.EnterPin(1234);
atm.WithdrawCash(100);

This would cause the following output to be printed:

Card inserted.
Correct PIN entered.
100 withdrawn.

If you tried to withdraw more than $500, you would see an error message:

ATM atm = new ATM();
atm.InsertCard();
atm.EnterPin(1234);
atm.WithdrawCash(1000);

Output:

Card inserted.
Correct PIN entered.
Error: Cannot withdraw more than $500 at a time.

Disadvantages

It’s important to carefully consider these potential disadvantages before deciding to use the State design pattern in your code. There are a few potential disadvantages to using the State design pattern:

  1. Increased complexity: Using the State design pattern can make the code more complex, especially if there are many different states and behaviors to implement.
  2. Performance overhead: The State design pattern involves creating multiple objects to represent the different states, which can add some performance overhead.
  3. Overuse: It’s possible to overuse the State design pattern, resulting in unnecessarily complex code.
  4. Maintenance: Maintaining the State design pattern can be more difficult because changes to the internal state of an object may require changes to multiple classes.

Conclusion

The main advantage of using the State design pattern is that it allows an object to change its behavior at runtime based on its internal state. This can be useful in situations where the behavior of an object depends on its state, and that state is likely to change at runtime.

Some additional benefits of using the State design pattern include:

  • It separates the responsibility for maintaining the internal state of an object from the responsibility for implementing the behavior associated with each state. This makes it easier to add new states and behaviors, and it also makes the code easier to understand and maintain.
  • It promotes the creation of reusable state objects.
  • It can make it easier to add new functionality to an object by encapsulating the behavior associated with each state in a separate class.

It’s important to consider whether the benefits of the State design pattern outweigh the additional complexity it adds to the code. In some cases, a simpler solution may be more appropriate.

Thanks for reading! If you found the article helpful, you can clap and follow. So you will notified of new articles.

# Reference

It was created with the help of ChatGPT AI.

--

--

Göksu Deniz

Software Engineer, passionate about creating efficient solutions. Skilled in mentoring teams to deliver successful projects. Always exploring new tech trends.