Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Що таке патерн State і чим він кращий за умови на кшталт if ($status === ...)?

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: стани як класи, збережені в колонці моделі, з описаними переходами й власними класами-переходами для логіки.

Докладніше в документації: Refactoring.Guru: State

Перевір себе

20 випадкових питань за спробу, після завершення - розбір кожної помилки

Схожі питання