State Pattern w C# — anatomia GoF i maszyna stanów zamówienia

Kolejny wzorzec z serii opartej na Anatomii wzorca — 8 elementach opisu GoF. Tym razem State — wzorzec, który zamienia rozrośnięty switch po statusie w zestaw małych klas, z których każda wie, jak zachować się w swoim stanie.
1. Intent (Zamierzenie)
Pozwala obiektowi zmienić zachowanie, gdy zmienia się jego stan wewnętrzny. Obiekt wygląda, jakby zmieniał swoją klasę.
Krótko: zamiast trzymać w obiekcie pole status i sprawdzać je w każdej metodzie, wydzielasz każdy stan do osobnej klasy. Obiekt deleguje pracę do bieżącego stanu, a zmiana stanu to podmiana jednego pola na inny obiekt.
2. Also Known As (Inne nazwy)
Objects for States — bo istota wzorca to potraktowanie każdego stanu jako pełnoprawnego obiektu, a nie wartości w enumie.
3. Motivation (Motywacja)
GoF opisuje połączenie TCP, które inaczej reaguje na te same żądania w zależności od tego, czy jest w stanie Established, Listening czy Closed. W .NET znasz to z modelu zamówienia: Zaplac() ma sens dla zamówienia Nowe, ale nie dla Wysłane; Anuluj() wolno wywołać przed wysyłką, ale nie po dostarczeniu.
Bez wzorca każda metoda Zamowienia zaczyna się od switch (Status) z gałęzią na każdy status — a ta sama drabinka warunków powtarza się w Zaplac, Wyslij, Anuluj. Dodanie statusu Zwrocone oznacza dopisanie gałęzi w kilku miejscach naraz. State zbiera zachowanie jednego stanu w jednej klasie.
4. Applicability (Kiedy stosować)
Stosuj State gdy:
- zachowanie obiektu zależy od jego stanu i musi zmieniać się w czasie działania,
- metody mają rozbudowane warunki (
if/switch) zależne od tego samego pola stanu, powtórzone w wielu miejscach, - przejścia między stanami mają jasne reguły, które chcesz mieć w jednym miejscu, a nie rozsiane po klasie.
5. Structure (Struktura)
┌────────────────┐ ┌──────────────────┐
│ Context │ ──────> │ <<interface>> │
│────────────────│ │ State │
│ -state │ │──────────────────│
│ +Request() │ │ +Handle() │
│ +SetState() │ └──────────────────┘
└────────────────┘ △
┌─────────────┼─────────────┐
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ConcreteA │ │ ConcreteB │ │ ConcreteC │
│──────────────│ │──────────────│ │──────────────│
│ +Handle() │ │ +Handle() │ │ +Handle() │
└──────────────┘ └──────────────┘ └──────────────┘Context trzyma referencję do obiektu State i deleguje do niego żądania. ConcreteState implementuje zachowanie dla swojego stanu i może przełączyć Context na inny stan.
6. Participants (Uczestnicy)
- Context — obiekt, którego zachowanie zależy od stanu (np.
Zamowienie). Trzyma referencję do bieżącegoStatei deleguje do niego pracę. - State — wspólny interfejs deklarujący operacje zależne od stanu.
- ConcreteState — implementuje zachowanie dla konkretnego stanu i decyduje o przejściach do innych stanów.
7. Collaborations (Współpraca)
Context deleguje żądania zależne od stanu do bieżącego obiektu ConcreteState. Context może przekazać siebie do State, żeby ten mógł zmienić stan. To Context lub ConcreteState decyduje, który stan następuje po którym.
Klient rozmawia wyłącznie z Context — nie widzi obiektów stanu ani tego, że w środku następuje podmiana.
8. Consequences (Konsekwencje)
Plus: zachowanie każdego stanu jest w jednej klasie, zamiast rozsiane po warunkach w wielu metodach. Plus: przejścia stają się jawne (widać, kto do czego przechodzi). Plus: nowy stan to nowa klasa, bez ruszania istniejących. Minus: rośnie liczba klas; dla 2-3 stabilnych stanów bywa to nadmiarowe wobec zwykłego
enum+switch.
—
State w C# — od diagramu do kodu
### Scenariusz
Zamówienie w sklepie przechodzi ścieżkę Nowe → Opłacone → Wysłane, z możliwością anulowania tylko przed wysyłką. Bez wzorca Zamowienie miałoby pole Status i switch w każdej metodzie — a reguły przejść byłyby rozsiane po całej klasie.
### State i Context
public interface IStanZamowienia
{
IStanZamowienia Zaplac();
IStanZamowienia Wyslij();
IStanZamowienia Anuluj();
string Nazwa { get; }
}
public class Zamowienie
{
private IStanZamowienia _stan = new StanNowe();
public string Status => _stan.Nazwa;
public void Zaplac() => _stan = _stan.Zaplac();
public void Wyslij() => _stan = _stan.Wyslij();
public void Anuluj() => _stan = _stan.Anuluj();
}Zamowienie nie zna reguł przejść — deleguje do bieżącego stanu i przyjmuje stan, który ten zwróci.
### ConcreteState — każdy stan zna swoje przejścia
public class StanNowe : IStanZamowienia
{
public string Nazwa => "Nowe";
public IStanZamowienia Zaplac() => new StanOplacone(); // dozwolone
public IStanZamowienia Wyslij() => throw new InvalidOperationException("Najpierw opłać.");
public IStanZamowienia Anuluj() => new StanAnulowane(); // wolno przed opłatą
}
public class StanOplacone : IStanZamowienia
{
public string Nazwa => "Opłacone";
public IStanZamowienia Zaplac() => throw new InvalidOperationException("Już opłacone.");
public IStanZamowienia Wyslij() => new StanWyslane(); // dozwolone
public IStanZamowienia Anuluj() => new StanAnulowane(); // jeszcze przed wysyłką
}
public class StanWyslane : IStanZamowienia
{
public string Nazwa => "Wysłane";
public IStanZamowienia Zaplac() => throw new InvalidOperationException("Już opłacone.");
public IStanZamowienia Wyslij() => throw new InvalidOperationException("Już wysłane.");
public IStanZamowienia Anuluj() => throw new InvalidOperationException("Nie anulujesz po wysyłce.");
}var z = new Zamowienie();
Console.WriteLine(z.Status); // Nowe
z.Zaplac();
Console.WriteLine(z.Status); // Opłacone
z.Wyslij();
Console.WriteLine(z.Status); // Wysłane
z.Anuluj(); // InvalidOperationException: Nie anulujesz po wysyłce.Reguła „nie anulujesz po wysyłce” żyje w jednym miejscu — w StanWyslane. Nowy stan Zwrocone to nowa klasa; żadna z istniejących się nie zmienia.
Idiom C# — kiedy wystarczy enum + switch
State w pełnej formie ma sens, gdy stanów jest wiele, mają własne zachowanie i dochodzą nowe. Dla kilku stabilnych stanów bez bogatego zachowania nadmiarem klas jest zwykły enum z switch expression:
public enum Status { Nowe, Opłacone, Wysłane }
Status Nastepny(Status s) => s switch
{
Status.Nowe => Status.Opłacone,
Status.Opłacone => Status.Wysłane,
_ => throw new InvalidOperationException("Brak dalszego przejścia.")
};To realna alternatywa oparta na pattern matchingu. Wybierasz pełny State, gdy każdy stan ma własną logikę (nie tylko „następny”), a liczba stanów rośnie.
Pułapka: State to nie Strategy
State i Strategy mają identyczny diagram UML — i to najczęstsze pytanie-pułapka na rozmowie. Różni je intencja:
- Strategy — to *klient* wybiera algorytm (np. sposób sortowania) i zwykle się on nie zmienia w trakcie życia obiektu. Strategie o sobie nie wiedzą.
- State — to *obiekt sam* przełącza stan w reakcji na zdarzenia, a stany znają siebie nawzajem (
StanNowewie, że po opłacie następujeStanOplacone). Przejścia są częścią wzorca.
Ta sama struktura, dwa różne problemy: wymienny algorytm vs zmieniające się w czasie zachowanie z przejściami.
Wariant: kto steruje przejściami
Powyżej przejścia zna sam stan (StanNowe.Zaplac() zwraca StanOplacone) — zaleta: reguła jest przy stanie; wada: stany zależą od siebie nawzajem. Alternatywa to przejścia w Context (mapa dozwolonych zmian) — wtedy stany są niezależne, ale wiedza o całej maszynie ląduje w jednym miejscu. Wybór zależy od tego, czy chcesz reguły przejść „rozproszone przy stanach”, czy „scentralizowane w kontekście”.
Podsumowanie
State zamienia pole status sprawdzane w wielkim switch na zestaw klas, gdzie każda odpowiada za zachowanie i przejścia jednego stanu. Context deleguje pracę do bieżącego stanu i nie zna reguł — te żyją w klasach stanów. Kluczowa różnica wobec Strategy (mimo identycznego diagramu): tu obiekt sam zmienia stan w reakcji na zdarzenia, a stany znają swoje przejścia. Gdy stanów jest mało i nie mają własnego zachowania, enum + switch expression jest lżejszą, w pełni akceptowalną alternatywą.
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →