👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
Behavioral Design Pattern - State

Behavioral Design Pattern - State

Author - Abdul Rahman (Bhai)

Design Pattern

13 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

In this article, let's learn about State Design Pattern in .NET.

The State Pattern is a behavioral design pattern that lets an object alter its behavior when its state changes. Instead of using a large set of if-else or switch statements, state-specific behavior is delegated to separate state classes.

For example, consider a bank account that can be in a regular or overdrawn state. Each state has its own rules—when in an overdrawn state, withdrawals may be restricted. Adding more states, like a gold state for high balances, can make the logic even more complex. Using the state pattern helps to manage this complexity by organizing the behavior in individual classes.

Why we gonna do?

As an object's behavior grows more state-dependent, the code accumulates large if-else or switch blocks that check current state and branch accordingly. Each new state requires modifying every existing conditional chain, violating the Open/Closed Principle and making the code fragile and hard to read.

This tangled conditional logic also makes transitions between states opaque — it becomes difficult to reason about which state an object is in, how it got there, and what will trigger a change. Testing individual state behaviors in isolation is nearly impossible when they are all interleaved in a single class.

The State Pattern resolves this by encapsulating each state's behavior in its own class and letting the context delegate to the active state object at runtime. Adding a new state means adding a new class rather than modifying existing ones, keeping the codebase open for extension and closed for modification.

How we gonna do?

Structure

The State pattern involves several key components:

  1. Context: The main class that maintains the current state (e.g., BankAccount).
  2. State: An abstract base class or interface that defines behaviors for different states (e.g., BankAccountState).
  3. Concrete States: Implementations of the state that handle specific state behavior (e.g., RegularState, OverdrawnState).

// State base class
public abstract class BankAccountState(BankAccount account)
{
    protected BankAccount _account = account;

    public abstract void Deposit(decimal amount);
    public abstract void Withdraw(decimal amount);
}

// Regular state class
public class RegularState(BankAccount account) : BankAccountState(account)
{
    public override void Deposit(decimal amount)
    {
        _account.Balance += amount;
        Console.WriteLine("Deposited money in Regular State.");
    }

    public override void Withdraw(decimal amount)
    {
        _account.Balance -= amount;
        if (_account.Balance < 0)
            _account.State = new OverdrawnState(_account);
        Console.WriteLine("Withdrew money in Regular State.");
    }

    override public string ToString()
    {
        return "Regular State";
    }
}

// Overdrawn state class
public class OverdrawnState(BankAccount account) : BankAccountState(account)
{
    public override void Deposit(decimal amount)
    {
        _account.Balance += amount;
        if (_account.Balance >= 0)
            _account.State = new RegularState(_account);
        Console.WriteLine("Deposited money in Overdrawn State.");
    }

    public override void Withdraw(decimal amount)
    {
        Console.WriteLine("Cannot withdraw in Overdrawn State.");
    }

    override public string ToString()
    {
        return "Overdrawn State";
    }
}

// Context class
public class BankAccount
{
    public decimal Balance { get; set; }
    public BankAccountState State { get; set; }

    public BankAccount()
    {
        Balance = 200;
        State = new RegularState(this);
    }

    public void Deposit(decimal amount) => State.Deposit(amount);
    public void Withdraw(decimal amount) => State.Withdraw(amount);
}
            
Demo Space

In the above code, see how state changes automatically based on the balance. When the balance is less than zero, the account is in an Overdrawn state, and withdrawals are restricted. When the balance is greater than zero, the account is in a Regular state, and withdrawals are allowed.

Use Cases

The State pattern is beneficial in several scenarios:

  1. Document editing: Different modes (insert, overwrite, select) with distinct behaviors.
  2. Vending machine: Different states like idle, selecting, dispensing, and their associated actions.
  3. Workflow management: Tasks change behavior based on whether they are "pending," "in progress," or "completed."
  4. Robotics: Operational modes like "exploration" or "charging" that affect robot behavior.

Advantages

  • Positive: Each state-specific behavior is isolated, making it easier to manage transitions and add new states.

Disadvantages

  • Negative: Adding more states increases the number of classes, leading to more complexity.

The State pattern is closely related to several other design patterns:

  • Flyweight: State objects can sometimes be shared across contexts, reducing memory usage.
  • Singleton: Often, state objects can be implemented as singletons since the state is shared.
  • Strategy: Similar to the state pattern, but strategies don't depend on internal state changes; they are chosen externally.
  • Bridge: Both patterns share the idea of composition to separate functionality, although they solve different problems.

Summary

The State Pattern allows you to manage complex state-dependent behaviors in a clean and scalable way. By encapsulating states in their own classes, you reduce the complexity of conditional logic and make the code easier to extend.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Design Pattern
  • Behavioral
  • State