Przejdź do treści

Wzorzec State w C#

  • przez

Wzorzec State pozwala obiektowi zmieniać swoje zachowanie w czasie działania w zależności od jego wewnętrznego stanu. Obiekt wygląda wtedy tak, jakby zmienił swoją klasę. W tym poście zaprezentuję jak go użyć w C# i kiedy jest on pomocny.

Struktura UML wzorca State może wyglądać tak:

  • Context Obiekt, którego zachowanie zależy od stanu. Przechowuje aktualny obiekt stanu i deleguje do niego zachowanie.
  • State Interfejs bazowy dla wszystkich stanów. Deklaruje metodę, którą stany muszą implementować.
  • ConcreteStateA/B Konkretne implementacje stanu. Zawierają logikę zależną od danego stanu oraz mogą zmieniać stan kontekstu.

Dla mnie najważniejsze zastosowania tego wzorca to:

  • Kiedy zachowanie obiektu zależy od jego stanu wewnętrznego.
  • Gdy wiele metod w klasie zawiera switch/if w zależności od stanu.
  • Gdy chcesz unikać rozrastających się klas z instrukcjami warunkowymi.

Kontynuując do zaler zaliczyłbym to:

  • Eliminuje rozbudowane bloki if/else lub switch.
  • Każdy stan jest samodzielną klasą – SRP (Single Responsibility Principle).
  • Łatwo dodać nowe stany bez modyfikowania istniejącego kodu (Open/Closed Principle).
  • Stan może być dynamicznie zmieniany w czasie działania programu.

Ale moim zdaniem ma też wady (ale tylko jeśli mówimy o prostym i małoskomplikowanym projekcie):

  • Może zwiększyć liczbę klas.
  • Potrzeba przemyślanej struktury – niepotrzebne zastosowanie może prowadzić do „overengineeringu”.

Oczywiście można nie używać tego wzorca i zastąpić wszystko kombinacjami if i switch. Ale w skomplikowanym projekcie po pewnym czasie dojdziemy do jeszce bardziej skompilkowanych warunków 😀 A napiszę więcej – skomplikowanych zagnieżdżonych warunków 🙂 Więc jeśli zależy Ci na tym, żeby Twój projekt był łatwy w utrzymaniu to rozważ zastosowanie tego wzorca.

Przykład

Implementację tego wzorca pokażę na prostym przykładzie stanu zamówienia. Stan zamówienia będzie rezprezentował interfejs IOrderState

public interface IOrderState
{
    void Proceed(Order context);
    void Cancel(Order context);
    string Name { get; }
}

Teraz czas na context, czyli klasę która będzie „trzymała” stan:

public class Order
{
    private IOrderState _state;

    public Order()
    {
        _state = new CreatedState(); // stan początkowy
    }

    public void SetState(IOrderState state)
    {
        Console.WriteLine($"State changed: {_state.Name} → {state.Name}");
        _state = state;
    }

    public void Proceed() => _state.Proceed(this);
    public void Cancel() => _state.Cancel(this);
    public string CurrentState => _state.Name;
}

No to teraz najważniejsze, czyli konkretne stany:

public class CreatedState : IOrderState
{
    public string Name => "Created";

    public void Proceed(Order context)
    {
        context.SetState(new PaidState());
    }

    public void Cancel(Order context)
    {
        context.SetState(new CancelledState());
    }
}

public class PaidState : IOrderState
{
    public string Name => "Paid";

    public void Proceed(Order context)
    {
        context.SetState(new ShippedState());
    }

    public void Cancel(Order context)
    {
        Console.WriteLine("Can't cancel. Already paid.");
    }
}

public class ShippedState : IOrderState
{
    public string Name => "Shipped";

    public void Proceed(Order context)
    {
        Console.WriteLine("Order is already shipped.");
    }

    public void Cancel(Order context)
    {
        Console.WriteLine("Can't cancel. Already shipped.");
    }
}

public class CancelledState : IOrderState
{
    public string Name => "Cancelled";

    public void Proceed(Order context)
    {
        Console.WriteLine("Can't proceed. Order is cancelled.");
    }

    public void Cancel(Order context)
    {
        Console.WriteLine("Already cancelled.");
    }
}

No i tyle 🙂 A teraz krótki przykład działania:

public static class Program
{
    public static void Main()
    {
        var order = new Order();

        Console.WriteLine($"Current state: {order.CurrentState}");
        order.Proceed(); // Created → Paid
        Console.WriteLine($"Current state: {order.CurrentState}");
        order.Proceed(); // Paid → Shipped
        Console.WriteLine($"Current state: {order.CurrentState}");
        order.Cancel();  // Too late to cancel
    }
}

Podsumowanie

Jak widać mamy trochę więcej kodu/klas ale to jest prosty przykład. Pomyśl ile miałbyś ifów bez tych klas a teraz pomyśl o tym samym gdybyś miał 20 stanów. Moim zdaniem kod z ifami byłby szybko zrozumiały tylko dla autora a dla kogoś kto pierwszy raz widziałby ten kod będzie on nie za bardzo zrozumiały 🙂