State (Стан) - поведінка об'єкта змінюється залежно від його стану, а кожен стан оформлений окремим класом. Замість умов у кожному методі об'єкт делегує поведінку поточному стану.
Без патерну - умови розповзаються по коду:
class Order
{
public function cancel(): void
{
if ($this->status === 'shipped' || $this->status === 'delivered') {
throw new CannotCancel();
}
if ($this->status === 'paid') {
$this->refund();
}
$this->status = 'cancelled';
}
public function ship(): void
{
if ($this->status !== 'paid') { throw new CannotShip(); }
// ...
}
// і так у кожному методі
}
Новий стан («очікує оплати частинами») - правка всіх методів, легко пропустити перевірку в одному з них.
З патерном State:
abstract class OrderState
{
public function cancel(Order $order): void { throw new InvalidTransition(static::class, 'cancel'); }
public function ship(Order $order): void { throw new InvalidTransition(static::class, 'ship'); }
}
final class Paid extends OrderState
{
public function cancel(Order $order): void
{
$order->refund();
$order->transitionTo(new Cancelled());
}
public function ship(Order $order): void
{
$order->transitionTo(new Shipped());
}
}
final class Shipped extends OrderState { /* cancel не дозволено - поведінка за замовчуванням */ }
Переваги:
- усі правила стану в одному класі: що дозволено зі стану «Оплачено», видно в класі
Paid; - недозволені переходи - помилка за замовчуванням, а не забута перевірка;
- новий стан - новий клас, наявні не змінюються (OCP);
- легко тестувати кожен стан окремо.
Простіша альтернатива - енум зі списком переходів:
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
case Cancelled = 'cancelled';
public function canTransitionTo(self $next): bool
{
return in_array($next, match ($this) {
self::Pending => [self::Paid, self::Cancelled],
self::Paid => [self::Shipped, self::Cancelled],
self::Shipped, self::Cancelled => [],
}, true);
}
}
Як обрати:
- різниться лише набір дозволених переходів - енум з таблицею переходів;
- різниться поведінка в кожному стані (різні побічні ефекти, розрахунки, доступні дії) - класи станів.
У Laravel готове рішення - spatie/laravel-model-states: стани як класи, збережені в колонці моделі, з описаними переходами й власними класами-переходами для логіки.