Wzorzec projektowy Stan (ang. State) jest jednym z kluczowych wzorców behawioralnych w programowaniu obiektowym.
Upraszcza zarządzanie stanami obiektów i pozwala na bardziej elastyczne tworzenie aplikacji, które w różny sposób reagują na te same wywołania w zależności od aktualnego stanu.
Czym jest wzorzec “Stan”?
Wzorzec “Stan” umożliwia zmianę zachowania obiektu w zależności od jego aktualnego stanu, bez konieczności stosowania skomplikowanych instrukcji warunkowych (np. if, switch).
Obiekt zyskuje więc elastyczność, a zmiana zachowań w trakcie działania aplikacji jest osiągana poprzez wymianę wewnętrznego stanu obiektu.
Krótka historia wzorca
Wzorzec “Stan” został szerzej opisany w książce Design Patterns: Elements of Reusable Object-Oriented Software, która została opublikowana w 1994 roku przez tzw. “Gang Czterech” (Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides).
Wzorzec ten stał się popularny w językach takich jak C++, Java czy PHP, szczególnie w aplikacjach, które obsługują skomplikowane przejścia pomiędzy stanami, np. systemy obsługujące różne tryby, maszyny stanów czy aplikacje z zaawansowaną logiką biznesową.
Kiedy stosować wzorzec Stan?
Wzorzec Stan warto stosować w sytuacjach, gdy:
- obiekt musi zmieniać swoje zachowanie w zależności od aktualnego stanu,
- kod staje się złożony przez dużą liczbę instrukcji warunkowych (np. if-else, switch),
- istnieje potrzeba dodawania nowych stanów bez ingerencji w istniejący kod,
- aplikacja wymaga przechowywania i zmiany stanów w sposób dynamiczny.
Typowe przypadki użycia obejmują systemy takie jak automaty biletowe, gry komputerowe, interfejsy użytkownika, czy aplikacje finansowe.
Korzyści ze stosowania wzorca Stan
Czytelność kodu
Logika stanów jest przenoszona do osobnych klas, co sprawia, że kod jest łatwiejszy do zrozumienia.
Elastyczność
Łatwo można dodawać nowe stany, nie modyfikując istniejącego kodu.
Unikanie instrukcji warunkowych
Pozwala wyeliminować złożone instrukcje warunkowe, które mogą być trudne w utrzymaniu i zrozumieniu.
Łatwiejsze testowanie
Każdy stan jest niezależnym obiektem, co ułatwia testowanie poszczególnych stanów.
Jak rozpoznać możliwość użycia wzorca Stan?
Jeśli napotykasz fragmenty kodu pełne skomplikowanych instrukcji warunkowych uzależnionych od stanu obiektu np.
if ($status === 'OPEN') { ... }
to może być znak, że warto rozważyć użycie wzorca “Stan”.
Dodatkowo, jeśli zmiana zachowania obiektu zależy od jego stanu, ale zmiana ta jest trudna do wdrożenia bez modyfikacji istniejących metod, to również wzorzec Stan może okazać się pomocny.
Implementacja wzorca Stan w PHP
Przejdźmy teraz do przykładu implementacji wzorca Stan w PHP, który zilustruje jego działanie na podstawie przykładu prostego automatu sprzedającego.
Automat może być w jednym z trzech stanów:
- Gotowy do przyjęcia monety
- Przyjmujący monetę
- Wydający produkt
// 1. Interfejs dla stanu interface VendingMachineState { public function insertCoin(): string; public function pressButton(): string; public function dispense(): string; } // 2. Klasy reprezentujące różne stany automatu class ReadyState implements VendingMachineState { public function __construct( private VendingMachine $machine ) { } public function insertCoin(): string { $this->machine->setState($this->machine->getHasCoinState()); return "Moneta przyjęta.\n"; } public function pressButton(): string { return "Nie można nacisnąć przycisku bez monety.\n"; } public function dispense(): string { return "Najpierw włóż monetę.\n"; } } class HasCoinState implements VendingMachineState { public function __construct( private VendingMachine $machine ) { } public function insertCoin(): string { return "Moneta została już przyjęta.\n"; } public function pressButton(): string { $this->machine->setState($this->machine->getSoldState()); return "Przycisk naciśnięty. Wydawanie produktu...\n"; } public function dispense(): string { return "Naciśnij przycisk, aby otrzymać produkt.\n"; } } class SoldState implements VendingMachineState { public function __construct( private VendingMachine $machine ) { } public function insertCoin(): string { return "Proszę czekać na zakończenie poprzedniej transakcji.\n"; } public function pressButton(): string { return "Produkt już wydany. Poczekaj chwilę.\n"; } public function dispense(): string { $this->machine->setState($this->machine->getReadyState()); return "Produkt wydany. Przełączenie na stan gotowy.\n"; } } // 3. Klasa VendingMachine, która przechowuje aktualny stan class VendingMachine { private $readyState; private $hasCoinState; private $soldState; private $currentState; public function __construct() { $this->readyState = new ReadyState($this); $this->hasCoinState = new HasCoinState($this); $this->soldState = new SoldState($this); $this->currentState = $this->readyState; } public function setState(VendingMachineState $state) { $this->currentState = $state; } public function getReadyState() { return $this->readyState; } public function getHasCoinState() { return $this->hasCoinState; } public function getSoldState() { return $this->soldState; } public function insertCoin() { $this->currentState->insertCoin(); } public function pressButton() { $this->currentState->pressButton(); } public function dispense() { $this->currentState->dispense(); } } // 4. Przykładowe użycie $machine = new VendingMachine(); $machine->insertCoin(); // Moneta przyjęta. $machine->pressButton(); // Przycisk naciśnięty. Wydawanie produktu... $machine->dispense(); // Produkt wydany. Przełączenie na stan gotowy.
Kolejny przykład: Zarządzanie stanem zamówienia w e-commerce
Przyjmijmy, że mamy prosty system zamówień, w którym zamówienie może przechodzić przez kilka etapów.
Początkowo jest w stanie “Nowe” (oczekuje na zatwierdzenie), potem przechodzi do stanu “Przetwarzane” (gdy rozpoczęto realizację), a na końcu staje się “Zrealizowane”.
Implementacja wzorca Stan w PHP
- Interfejs stanu zamówienia – definiuje wspólne operacje dla wszystkich stanów.
- Klasy stanów – każda z nich reprezentuje inny stan zamówienia.
- Klasa Zamówienia – przechowuje aktualny stan zamówienia oraz umożliwia jego zmianę.
// 1. Interfejs stanu zamówienia interface OrderState { public function proceedToNext(Order $order): string; public function cancel(Order $order): string; public function getStatus(): string; } // 2. Klasy reprezentujące różne stany zamówienia class NewOrderState implements OrderState { public function proceedToNext(Order $order): string { $order->setState(new ProcessingOrderState()); return "Zamówienie jest teraz przetwarzane.\n"; } public function cancel(Order $order): string { $order->setState(new CancelledOrderState()); return "Anulowanie nowego zamówienia.\n"; } public function getStatus(): string { return "Nowe"; } } class ProcessingOrderState implements OrderState { public function proceedToNext(Order $order): string { $order->setState(new CompletedOrderState()); return "Zamówienie zostało zrealizowane.\n"; } public function cancel(Order $order): string { return "Nie można anulować zamówienia w trakcie przetwarzania.\n"; } public function getStatus(): string { return "Przetwarzane"; } } class CompletedOrderState implements OrderState { public function proceedToNext(Order $order): string { return "Zamówienie zostało już zrealizowane. Dalsze działania są zbędne.\n"; } public function cancel(Order $order): string { return "Nie można anulować zamówienia, które zostało zrealizowane.\n"; } public function getStatus(): string { return "Zrealizowane"; } } class CancelledOrderState implements OrderState { public function proceedToNext(Order $order): string { return "Zamówienie jest anulowane. Nie można przejść do kolejnych kroków.\n"; } public function cancel(Order $order): string { return "Zamówienie jest już anulowane.\n"; } public function getStatus(): string { return "Anulowane"; } } // 3. Klasa Order, która przechowuje aktualny stan zamówienia class Order { // Domyślny stan nowego zamówienia public function __construct( private OrderState $state ) { } public function setState(OrderState $state) { $this->state = $state; } public function proceedToNext() { $this->state->proceedToNext($this); } public function cancel() { $this->state->cancel($this); } public function getStatus(): string { return $this->state->getStatus(); } } // 4. Przykładowe użycie $order = new Order(new NewOrderState()); echo "Aktualny stan zamówienia: " . $order->getStatus() . "\n"; // Nowe $order->proceedToNext(); // Przejście do przetwarzania echo "Aktualny stan zamówienia: " . $order->getStatus() . "\n"; // Przetwarzane $order->proceedToNext(); // Przejście do zrealizowania echo "Aktualny stan zamówienia: " . $order->getStatus() . "\n"; // Zrealizowane $order->cancel(); // Próbujemy anulować zrealizowane zamówienie
W powyższym przykładzie:
- Zamówienie zaczyna od stanu “Nowe” i może przejść do stanu “Przetwarzane” lub zostać anulowane.
- Stan “Przetwarzane” pozwala na przejście do “Zrealizowane”, ale uniemożliwia anulowanie zamówienia.
- Stan “Zrealizowane” oznacza, że zamówienie nie może być już anulowane ani zmienione.
- Jeśli zamówienie jest anulowane, nie można przejść do żadnego kolejnego stanu.
W ten sposób logika przejść pomiędzy stanami jest izolowana w osobnych klasach, co pozwala zachować czytelność kodu, elastyczność oraz możliwość łatwego dodania nowych stanów, jeśli zajdzie taka potrzeba.
Jak zaimplementować wzorzec Stan w istniejącym kodzie?
- Identyfikacja stanów: Zidentyfikuj możliwe stany obiektu.
- Utwórz interfejs stanów: Stwórz interfejs definiujący wspólne operacje dla wszystkich stanów.
- Zaimplementuj klasy stanów: Dla każdego stanu utwórz osobną klasę implementującą interfejs stanów.
- Integracja klas stanów z klasą kontekstu: Klasa kontekstu powinna posiadać referencję do obiektów stanów oraz metody do zmiany stanu.
Podsumowanie
Wzorzec projektowy “Stan” jest niezwykle przydatnym narzędziem w PHP, które pozwala tworzyć elastyczny i czytelny kod poprzez rozdzielenie logiki stanów do osobnych klas.
Dzięki temu łatwo zarządzać zachowaniem obiektu w zależności od jego aktualnego stanu, bez stosowania rozbudowanych instrukcji warunkowych.
Wprowadzenie wzorca Stan nie tylko poprawia strukturę kodu, ale również ułatwia jego rozszerzalność oraz testowanie, dzięki czemu staje się on bardziej skalowalny i łatwy w utrzymaniu.
Hejka.
Przykłady wyglądają z pozoru dobrze, ale czy próbowałeś je uruchomić?
W tym drugim (status zamówienia) wyskakuje błąd:
„PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function Order::__construct(), 0 passed …”.
W linii:
$order = new Order(); <– brakuje argumentu
Po zmianie na:
$order = new Order(new NewOrderState());
działa 🙂
Poza tym dobry artykuł i … pozostałe też zaczynam czytać 🙂
Pozdrawiam i wytrwałości w pisaniu życzę.
Cześć, dziękuję że napisałeś 🙂 Pisałem to z palca – nie sprawdzałem i faktycznie mały błąd się pojawił.