C# State Pattern

Summary: in this tutorial, you will learn how to use the C# State pattern to allow an object to change its behavior when its internal state changes.

Introduction to the C# State pattern

The State pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes.

The State pattern is useful when the behavior of an object depends on its state and needs to change dynamically when its state changes.

The following UML diagram illustrates the State pattern:

C# State Pattern

The State pattern typically consists of the following participants:

  • Context is an object that maintains the current state and provides an interface for the clients to interact with the object.
  • IState is an interface that defines methods that the concrete states must implement.
  • ConcreteState is the concrete class that provides a specific implementation of the IState interface. Each ConcreteState class represents the different behavior of the object.

When the state of the context changes, it delegates the behavior to the current ConcreteState object. The ConcreteState class implements the behavior associated with the state. It allows the object to change its behavior dynamically without changing its interface.

C# State pattern example

Suppose you need to manage invoices with various states: unpaid, paid, canceled, and refunded:

C# State Pattern Example - Invoice States

The following program illustrates how to use the State pattern to manage the behavior of invoices which changes dynamically based on their states:

namespace StatePattern;

public interface IInvoiceState
{
    void Pay(Invoice invoice);
    void Cancel(Invoice invoice);
    void Refund(Invoice invoice);
}

public class Invoice
{
    public int Number
    {
        get;
    }
    public decimal Amount
    {
        get;
    }
    public string Description
    {
        get;
    }
    public IInvoiceState State
    {
        get; set;
    }

    public Invoice(int number, decimal amount, string description)
    {
        Number = number;
        Amount = amount;
        Description = description;

        // Set the invoice as Unpaid
        State = new UnpaidState();
    }


    public void Pay()
    {
        State.Pay(this);
        State = new PaidState();
    }

    public void Cancel()
    {
        State.Cancel(this);
        State = new CanceledState();
    }

    public void Refund()
    {
        State.Refund(this);
        State = new RefundedState();
    }
}


public class UnpaidState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Paying invoice {invoice.Number}...");

    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Canceling invoice {invoice.Number}...");

    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} is unpaid and cannot be refunded.");

    }
}

public class PaidState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has already been paid.");
    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} cannot be cancelled.");
    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been refunded.");
    }
}

public class CanceledState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been canceled and cannot be paid.");
    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has already been canceled and cannot be canceled again.");
    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been canceled and cannot be refunded.");
    }
}

public class RefundedState : IInvoiceState
{
    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} was refunded and cannot be cancelled.");

    }
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} was refunded and cannot be paid.");

    }
    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} cannot be refunded again.");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        var invoice = new Invoice(123, 1000m, "Software Dev Services");

        invoice.Pay();
        invoice.Refund();
    }
}Code language: C# (cs)

Output:

Paying invoice 123...
Invoice 123 has been refunded.Code language: C# (cs)

How it works.

First, define the IInvoiceState interface that has three methods Pay, Cancel, and Refund:

public interface IInvoiceState
{
    void Pay(Invoice invoice);
    void Cancel(Invoice invoice);
    void Refund(Invoice invoice);
}Code language: C# (cs)

Second, define the Invoice class that has four properties Number, Amount, Description, and State. Note that this is for simplicity purposes. In the real-world application, an invoice may contain more information. The constructor of the Invoice class initializes the State property to the UnpaidState object:

public class Invoice
{
    public int Number
    {
        get;
    }
    public decimal Amount
    {
        get;
    }
    public string Description
    {
        get;
    }
    public IInvoiceState State
    {
        get; set;
    }

    public Invoice(int number, decimal amount, string description)
    {
        Number = number;
        Amount = amount;
        Description = description;

        // Set the invoice as Unpaid
        State = new UnpaidState();
    }


    public void Pay()
    {
        State.Pay(this);
        State = new PaidState();
    }

    public void Cancel()
    {
        State.Cancel(this);
        State = new CanceledState();
    }

    public void Refund()
    {
        State.Refund(this);
        State = new RefundedState();
    }
}Code language: C# (cs)

Third, define the UnpaidState class that implements the IInvoiceState interface:

public class UnpaidState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Paying invoice {invoice.Number}...");

    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Canceling invoice {invoice.Number}...");

    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} is unpaid and cannot be refunded.");

    }
}Code language: C# (cs)

An unpaid invoice can be paid or canceled but cannot be refunded.

Fourth, define the PaidState class that also implements the IInvoiceState interface:

public class PaidState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has already been paid.");
    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} cannot be cancelled.");
    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been refunded.");
    }
}Code language: C# (cs)

A paid invoice cannot be paid again or canceled but can be refunded.

Fifth define the CanceledState class that implements the IInvoiceState interface:

public class CanceledState : IInvoiceState
{
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been canceled and cannot be paid.");
    }

    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has already been canceled and cannot be canceled again.");
    }

    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} has been canceled and cannot be refunded.");
    }
}Code language: C# (cs)

A canceled invoice cannot be canceled, refunded, or paid.

Similarly, a refunded invoice cannot be canceled, refunded, or paid:

public class RefundedState : IInvoiceState
{
    public void Cancel(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} was refunded and cannot be cancelled.");

    }
    public void Pay(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} was refunded and cannot be paid.");

    }
    public void Refund(Invoice invoice)
    {
        Console.WriteLine($"Invoice {invoice.Number} cannot be refunded again.");
    }
}Code language: C# (cs)

Finally, create an invoice object and call the Pay() and Refund() method sequentially:

public class Program
{
    public static void Main(string[] args)
    {
        var invoice = new Invoice(123, 1000m, "Software Dev Services");

        invoice.Pay();
        invoice.Refund();
    }
}Code language: C# (cs)

Summary

  • Use the State pattern to let an object change its behavior when its internal state changes.
Was this tutorial helpful ?