Introduction
The state machine design pattern is an effective approach for managing systems with multiple states, providing a clear structure for state transitions and their associated logic.
In order to illustrate the benefits of this approach, let's first begin with an example, without using the pattern.
Problem
Suppose we want to implement a Super Mario video game. The main character Mario can be in one of the following states:
public enum MarioState
{
Dead,
Small,
Super,
Invincible
}
public enum MarioState
{
Dead,
Small,
Super,
Invincible
}
Mario can also encounter various items and enemies, which will affect the state as depicted by the following diagram:
We might write some code like the following:
public class Mario
{
private MarioState currentState;
// ...
public void ObtainMushroom()
{
switch (currentState)
{
case MarioState.Small:
Console.WriteLine("Small Mario obtained a 🍄 mushroom! Powering up to Super Mario.");
currentState = MarioState.Super;
break;
case MarioState.Super:
Console.WriteLine("Super Mario obtained a 🍄 mushroom! Nothing happens as Super Mario.");
break;
case MarioState.Invincible:
Console.WriteLine("Invincible Mario obtained a 🍄 mushroom! Nothing happens as Invincible Mario.");
break;
default:
break;
}
}
// ...
}
public class Mario
{
private MarioState currentState;
// ...
public void ObtainMushroom()
{
switch (currentState)
{
case MarioState.Small:
Console.WriteLine("Small Mario obtained a 🍄 mushroom! Powering up to Super Mario.");
currentState = MarioState.Super;
break;
case MarioState.Super:
Console.WriteLine("Super Mario obtained a 🍄 mushroom! Nothing happens as Super Mario.");
break;
case MarioState.Invincible:
Console.WriteLine("Invincible Mario obtained a 🍄 mushroom! Nothing happens as Invincible Mario.");
break;
default:
break;
}
}
// ...
}
We can achieve this with conditional logic via switch statements.
However, if project requirements change (e.g. a new state is added), all the switch statements will need to be modified!
Solution
Let's see how we can refactor this using the state machine design pattern to make our code cleaner and more maintainable.
The key takeaway from this class diagram is that we encapsulate each state into its
own class, all inheriting from the base State
class.
Some additional notes are listed below.
- The
Context
class stores a reference to one of the concreteState
objects in order to delegate all state-specific work. - The
Context
also exposes a setter for passing it a new state object (i.e. when we need to perform a state transition) via theTransitionTo
method.
Refactored Example
Let's refactor our example to see what this would look like.
using System;
// This class represents the Context class, which stores a reference to one of the concrete State objects.
public class Mario
{
private MarioState state;
public Mario()
{
state = new SmallMarioState(this);
}
// The Context exposes a method to update the State at runtime.
public void TransitionTo(MarioState newState)
{
state = newState;
}
// The Context delegates logic to the State object.
public void ObtainMushroom()
{
state.ObtainMushroom();
}
public void ObtainStar()
{
state.ObtainStar();
}
public void CollideEnemy()
{
state.CollideEnemy();
}
}
using System;
// This class represents the Context class, which stores a reference to one of the concrete State objects.
public class Mario
{
private MarioState state;
public Mario()
{
state = new SmallMarioState(this);
}
// The Context exposes a method to update the State at runtime.
public void TransitionTo(MarioState newState)
{
state = newState;
}
// The Context delegates logic to the State object.
public void ObtainMushroom()
{
state.ObtainMushroom();
}
public void ObtainStar()
{
state.ObtainStar();
}
public void CollideEnemy()
{
state.CollideEnemy();
}
}
// The base State class declares methods that all concrete State should implement.
public abstract class MarioState
{
// The State class also provides a reference to the Context object.
// This reference can be used by States to transition the Context to another State.
protected Mario mario;
public MarioState(Mario mario)
{
this.mario = mario;
}
public abstract void ObtainMushroom();
public abstract void ObtainStar();
public abstract void CollideEnemy();
}
// The base State class declares methods that all concrete State should implement.
public abstract class MarioState
{
// The State class also provides a reference to the Context object.
// This reference can be used by States to transition the Context to another State.
protected Mario mario;
public MarioState(Mario mario)
{
this.mario = mario;
}
public abstract void ObtainMushroom();
public abstract void ObtainStar();
public abstract void CollideEnemy();
}
A little detail you might notice in the base state class is that there is a also a reference to the Mario
class, i.e. the Context
class.
This reference is necessary to transition the Context
to another State
.
public class SmallMarioState : MarioState
{
public SmallMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Small Mario obtained a 🍄 mushroom! Powering up to Super Mario.");
mario.TransitionTo(new SuperMarioState(mario));
}
public override void ObtainStar()
{
Console.WriteLine("Small Mario obtained a ⭐ star! Powering up to Invincible Mario.");
mario.TransitionTo(new InvincibleMarioState(mario));
}
public override void CollideEnemy()
{
Console.WriteLine("Small Mario collided with an enemy! Mario is dead ☠️.");
mario.TransitionTo(new DeadMarioState(mario));
}
}
public class SmallMarioState : MarioState
{
public SmallMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Small Mario obtained a 🍄 mushroom! Powering up to Super Mario.");
mario.TransitionTo(new SuperMarioState(mario));
}
public override void ObtainStar()
{
Console.WriteLine("Small Mario obtained a ⭐ star! Powering up to Invincible Mario.");
mario.TransitionTo(new InvincibleMarioState(mario));
}
public override void CollideEnemy()
{
Console.WriteLine("Small Mario collided with an enemy! Mario is dead ☠️.");
mario.TransitionTo(new DeadMarioState(mario));
}
}
public class SuperMarioState : MarioState
{
public SuperMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Super Mario obtained a 🍄 mushroom! Nothing happens as Super Mario.");
}
public override void ObtainStar()
{
Console.WriteLine("Super Mario obtained a ⭐ star! Powering up to Invincible Mario.");
mario.TransitionTo(new InvincibleMarioState(mario));
}
public override void CollideEnemy()
{
Console.WriteLine("Super Mario collided with an enemy! Powering down to Small Mario.");
mario.TransitionTo(new SmallMarioState(mario));
}
}
public class SuperMarioState : MarioState
{
public SuperMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Super Mario obtained a 🍄 mushroom! Nothing happens as Super Mario.");
}
public override void ObtainStar()
{
Console.WriteLine("Super Mario obtained a ⭐ star! Powering up to Invincible Mario.");
mario.TransitionTo(new InvincibleMarioState(mario));
}
public override void CollideEnemy()
{
Console.WriteLine("Super Mario collided with an enemy! Powering down to Small Mario.");
mario.TransitionTo(new SmallMarioState(mario));
}
}
public class InvincibleMarioState : MarioState
{
public InvincibleMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Invincible Mario obtained a 🍄 mushroom! Nothing happens.");
}
public override void ObtainStar()
{
Console.WriteLine("Invincible Mario obtained a ⭐ star! Nothing happens.");
}
public override void CollideEnemy()
{
Console.WriteLine("Invincible Mario collided with an enemy! Enemy defeated.");
}
}
public class InvincibleMarioState : MarioState
{
public InvincibleMarioState(Mario mario) : base(mario) { }
public override void ObtainMushroom()
{
Console.WriteLine("Invincible Mario obtained a 🍄 mushroom! Nothing happens.");
}
public override void ObtainStar()
{
Console.WriteLine("Invincible Mario obtained a ⭐ star! Nothing happens.");
}
public override void CollideEnemy()
{
Console.WriteLine("Invincible Mario collided with an enemy! Enemy defeated.");
}
}
// There can be additional logic for determining game over, etc.
// This was omitted for simplicity.
public class DeadMarioState : MarioState
{
public override void ObtainMushroom()
{
// Dead, so do nothing.
}
public override void ObtainStar()
{
// Dead, so do nothing.
}
public override void CollideEnemy()
{
// Dead, so do nothing.
}
}
// There can be additional logic for determining game over, etc.
// This was omitted for simplicity.
public class DeadMarioState : MarioState
{
public override void ObtainMushroom()
{
// Dead, so do nothing.
}
public override void ObtainStar()
{
// Dead, so do nothing.
}
public override void CollideEnemy()
{
// Dead, so do nothing.
}
}
And finally, the client can instantiate the Mario
class and call the methods as required:
class Program
{
static void Main()
{
Mario mario = new Mario();
mario.ObtainMushroom();
mario.ObtainStar();
mario.CollideEnemy();
}
}
class Program
{
static void Main()
{
Mario mario = new Mario();
mario.ObtainMushroom();
mario.ObtainStar();
mario.CollideEnemy();
}
}
Conclusion
✔️ Code is organized into distinct state classes instead of conditionals, improving readability and maintainability.
✔️ The pattern allows for extensibility (e.g. if we need to add a new state or make modifications to an existing state without impacting other states).