Wzorzec State pozwala obiektowi zmieniać swoje zachowanie gdy zmienia się jego wewnętrzny stan. Z zewnątrz wygląda jakby obiekt zmienił klasę. Jeśli masz klasę z rosnącym if-else który sprawdza „w jakim stanie jestem przed każdą operacją” – to klasyczny kandydat na refaktoring do wzorca State. Implementuję maszynę stanów dla zamówienia e-commerce.
Problem bez wzorca State
<?php
// Klasa Order bez wzorca State - if-else rośnie z każdym nowym stanem
class Order
{
private string $status = 'pending';
public function process(): void
{
if ($this->status === 'pending') {
$this->status = 'processing';
echo "Zamówienie w realizacji\n";
} elseif ($this->status === 'processing') {
throw new \LogicException('Nie można przetworzyć zamówienia w realizacji');
} elseif ($this->status === 'shipped') {
throw new \LogicException('Nie można przetworzyć wysłanego zamówienia');
}
// ... dla każdego nowego stanu musisz modyfikować tę metodę
}
public function ship(): void
{
if ($this->status === 'pending') {
throw new \LogicException('Nie można wysłać niepotwierdzonego zamówienia');
} elseif ($this->status === 'processing') {
$this->status = 'shipped';
echo "Zamówienie wysłane\n";
} elseif ($this->status === 'shipped') {
throw new \LogicException('Zamówienie już wysłane');
}
// ... kolejne powielone if-else
}
public function cancel(): void
{
if ($this->status === 'shipped') {
throw new \LogicException('Nie można anulować wysłanego zamówienia');
}
$this->status = 'cancelled';
}
}
// Problemy: naruszenie OCP, duplikacja logiki, trudne testowanie
Implementacja wzorca State
<?php
declare(strict_types=1);
// Interfejs stanu - definiuje wszystkie możliwe akcje
interface OrderStateInterface
{
public function process(Order $order): void;
public function ship(Order $order): void;
public function deliver(Order $order): void;
public function cancel(Order $order): void;
public function refund(Order $order): void;
public function getName(): string;
}
// Abstrakcyjny stan bazowy - domyślnie rzuca wyjątek
// Konkretne stany nadpisują tylko akcje które obsługują
abstract class AbstractOrderState implements OrderStateInterface
{
public function process(Order $order): void
{
throw new \LogicException(
"Nie można przetworzyć zamówienia w stanie: {$this->getName()}"
);
}
public function ship(Order $order): void
{
throw new \LogicException(
"Nie można wysłać zamówienia w stanie: {$this->getName()}"
);
}
public function deliver(Order $order): void
{
throw new \LogicException(
"Nie można potwierdzić dostarczenia w stanie: {$this->getName()}"
);
}
public function cancel(Order $order): void
{
throw new \LogicException(
"Nie można anulować zamówienia w stanie: {$this->getName()}"
);
}
public function refund(Order $order): void
{
throw new \LogicException(
"Nie można zwrócić zamówienia w stanie: {$this->getName()}"
);
}
}
<?php
declare(strict_types=1);
// Konkretne stany - każdy obsługuje tylko swoje możliwe przejścia
class PendingState extends AbstractOrderState
{
public function getName(): string { return 'pending'; }
public function process(Order $order): void
{
echo "Zamówienie #{$order->getId()} przekazane do realizacji\n";
$order->setState(new ProcessingState());
}
public function cancel(Order $order): void
{
echo "Zamówienie #{$order->getId()} anulowane przed realizacją\n";
$order->setState(new CancelledState());
}
}
class ProcessingState extends AbstractOrderState
{
public function getName(): string { return 'processing'; }
public function ship(Order $order): void
{
echo "Zamówienie #{$order->getId()} wysłane do klienta\n";
$order->setState(new ShippedState());
}
public function cancel(Order $order): void
{
echo "Zamówienie #{$order->getId()} anulowane w trakcie realizacji\n";
$order->setState(new CancelledState());
}
}
class ShippedState extends AbstractOrderState
{
public function getName(): string { return 'shipped'; }
public function deliver(Order $order): void
{
echo "Zamówienie #{$order->getId()} dostarczone\n";
$order->setState(new DeliveredState());
}
}
class DeliveredState extends AbstractOrderState
{
public function getName(): string { return 'delivered'; }
public function refund(Order $order): void
{
echo "Zwrot dla zamówienia #{$order->getId()} zainicjowany\n";
$order->setState(new RefundedState());
}
}
class CancelledState extends AbstractOrderState
{
public function getName(): string { return 'cancelled'; }
// Żadne przejścia nie są możliwe z tego stanu - wszystko rzuca wyjątek
}
class RefundedState extends AbstractOrderState
{
public function getName(): string { return 'refunded'; }
// Stan końcowy - brak dalszych przejść
}
<?php
declare(strict_types=1);
// Kontekst - klasa Order deleguje operacje do aktualnego stanu
class Order
{
private OrderStateInterface $state;
public function __construct(
private int $id
) {
// Stan początkowy
$this->state = new PendingState();
}
public function setState(OrderStateInterface $state): void
{
$previousState = $this->state->getName();
$this->state = $state;
echo "Zmiana stanu: {$previousState} -> {$state->getName()}\n";
}
public function getState(): OrderStateInterface { return $this->state; }
public function getStatus(): string { return $this->state->getName(); }
public function getId(): int { return $this->id; }
// Delegacja do stanu - Order nie wie jakie przejścia są możliwe
public function process(): void { $this->state->process($this); }
public function ship(): void { $this->state->ship($this); }
public function deliver(): void { $this->state->deliver($this); }
public function cancel(): void { $this->state->cancel($this); }
public function refund(): void { $this->state->refund($this); }
}
// Użycie - szczęśliwa ścieżka
$order = new Order(1042);
echo "Stan: " . $order->getStatus() . "\n\n"; // pending
$order->process(); // pending -> processing
$order->ship(); // processing -> shipped
$order->deliver(); // shipped -> delivered
$order->refund(); // delivered -> refunded
echo "\nStan końcowy: " . $order->getStatus() . "\n";
<?php
// Niedozwolone przejście - stan rzuca wyjątek
$order2 = new Order(1043);
$order2->process();
try {
$order2->ship(); // ok - processing -> shipped
$order2->ship(); // błąd - shipped nie ma metody ship
} catch (\LogicException $e) {
echo "Błąd: " . $e->getMessage() . "\n";
// Błąd: Nie można wysłać zamówienia w stanie: shipped
}
Serializacja stanu – zapis do bazy danych
<?php
declare(strict_types=1);
// Fabryka stanów - odtwarzanie obiektu stanu z nazwy (np. przy ładowaniu z bazy)
class OrderStateFactory
{
private array $states = [];
public function __construct()
{
$this->states = [
'pending' => new PendingState(),
'processing' => new ProcessingState(),
'shipped' => new ShippedState(),
'delivered' => new DeliveredState(),
'cancelled' => new CancelledState(),
'refunded' => new RefundedState(),
];
}
public function create(string $stateName): OrderStateInterface
{
return $this->states[$stateName]
?? throw new \InvalidArgumentException("Unknown state: {$stateName}");
}
}
// Repoztorium ładujące Order z bazy z właściwym stanem
class OrderRepository
{
public function __construct(
private \PDO $pdo,
private OrderStateFactory $stateFactory
) {}
public function getById(int $id): Order
{
$row = $this->pdo
->prepare('SELECT * FROM orders WHERE id = ?')
->execute([$id]);
$order = new Order((int) $row['id']);
// Przywróć stan z bazy - fabryka tworzy odpowiedni obiekt stanu
$state = $this->stateFactory->create($row['status']);
$order->setState($state);
return $order;
}
}
State vs Strategy – kiedy który?
| Aspekt | State | Strategy |
|---|---|---|
| Zmiana zachowania | Na podstawie wewnętrznego stanu obiektu | Na podstawie zewnętrznej konfiguracji |
| Kto zmienia stan/strategię | Sam obiekt (wewnętrznie) | Klient (z zewnątrz) |
| Stany wiedzą o sobie | Tak – PendingState wie że następny to ProcessingState | Nie – strategie są niezależne |
| Typowe użycie | Maszyna stanów, workflow zamówień, dokumentów | Wymienne algorytmy sortowania, płatności, wysyłki |
Podsumowanie
Wzorzec State eliminuje if-else spaghetti w klasach które mają logikę zależną od stanu wewnętrznego. Każdy stan to osobna klasa która wie jakie przejścia są dla niego możliwe i co zrobić gdy ktoś próbuje wykonać niedozwoloną operację. Dodanie nowego stanu to nowa klasa i zero zmian w istniejącym kodzie – czyste Open/Closed. W Magento 2 podobny mechanizm znajdziesz przy statusach zamówień – choć tam jest bardziej konfiguracyjny niż oparty na obiektach stanu.
