Menu
Coddy logo textTech

Паттерн Состояние

Часть раздела Объектно-ориентированное программирование путешествия по PHP на Coddy — урок 79 из 91.

Паттерн Состояние (State Pattern) — это поведенческий паттерн проектирования, который позволяет объекту изменять свое поведение при изменении его внутреннего состояния. Вместо использования сложных условных операторов для обработки различных состояний, вы инкапсулируете каждое состояние в отдельный класс и делегируете поведение текущему объекту состояния.

Представьте себе торговый автомат. Он ведет себя по-разному в зависимости от того, ожидает ли он оплаты, получил ли деньги или выдает товар.

У каждого состояния есть свои правила относительно того, какие действия разрешены. Паттерн «Состояние» (State Pattern) моделирует это, создавая класс для каждого состояния.

<?php
interface OrderState {
    public function proceed(Order $order): string;
    public function getStatus(): string;
}

class PendingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ProcessingState());
        return "Order moved to processing";
    }
    
    public function getStatus(): string {
        return "Pending";
    }
}

class ProcessingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ShippedState());
        return "Order has been shipped";
    }
    
    public function getStatus(): string {
        return "Processing";
    }
}

class ShippedState implements OrderState {
    public function proceed(Order $order): string {
        return "Order already shipped";
    }
    
    public function getStatus(): string {
        return "Shipped";
    }
}

Класс контекста хранит ссылку на текущее состояние и делегирует ему вызовы:

<?php
class Order {
    private OrderState $state;
    
    public function __construct() {
        $this->state = new PendingState();
    }
    
    public function setState(OrderState $state): void {
        $this->state = $state;
    }
    
    public function proceed(): string {
        return $this->state->proceed($this);
    }
    
    public function getStatus(): string {
        return $this->state->getStatus();
    }
}

$order = new Order();
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus() . "\n";
echo $order->proceed() . "\n";
echo $order->getStatus();

Вывод:

Pending
Order moved to processing
Processing
Order has been shipped
Shipped

Паттерн Состояние устраняет разрастающиеся операторы switch и делает добавление новых состояний простым. Каждый класс состояния отвечает за знание того, какое состояние следует за ним, сохраняя логику организованной и удобной для поддержки.

challenge icon

Задание

Легко

Давайте создадим систему документооборота, используя паттерн «Состояние» (State Pattern). Документы в системе управления контентом проходят через различные этапы — черновик, проверка и публикация — и на каждом этапе действуют свои правила относительно разрешенных действий и того, что происходит при попытке продвинуть документ дальше.

Вы распределите свой код по четырем файлам:

  • DocumentState.php — Определите интерфейс DocumentState, который устанавливает контракт для всех состояний документа. Он должен иметь два метода: publish(Document $document): string для попытки продвижения документа и getStatus(): string для возврата названия текущего состояния.
  • States.php — Подключите интерфейс DocumentState и создайте три класса состояний, реализующих его:
    • DraftState — При вызове publish() переводит документ в состояние ReviewState и возвращает "Document sent for review". Его статус — "Draft".
    • ReviewState — При вызове publish() переводит документ в состояние PublishedState и возвращает "Document published". Его статус — "Under Review".
    • PublishedState — При вызове publish() возвращает "Document is already published" без изменения состояния. Его статус — "Published".
  • Document.php — Подключите файл States и создайте класс Document, который выступает в роли контекста. Ваш документ должен:
    • Принимать заголовок в конструкторе и начинаться в состоянии DraftState
    • Иметь метод setState(DocumentState $state): void для изменения текущего состояния
    • Иметь метод publish(): string, который делегирует выполнение методу publish() текущего состояния
    • Иметь метод getStatus(): string, который возвращает статус текущего состояния
  • main.php — Подключите файл Document. Вы получите на вход одно число: количество вызовов publish() для документа.

    Создайте Document с заголовком "My Article". Сначала выведите начальный статус. Затем вызовите publish() указанное количество раз, выводя результат каждого вызова на новой строке. В конце выведите итоговый статус.

    Формат вывода:

    Status: [initial status]
    [publish result 1]
    [publish result 2]
    ...
    Status: [final status]

Каждый класс состояния знает, какое состояние следует за ним в рабочем процессе, что делает логику переходов организованной и легкой для изменения. Добавление нового состояния, например "Archived", будет означать просто создание нового класса и обновление соответствующих переходов.

Шпаргалка

Паттерн Состояние (State Pattern) — это поведенческий паттерн проектирования, который позволяет объекту изменять свое поведение при изменении его внутреннего состояния путем инкапсуляции каждого состояния в отдельный класс.

Определите интерфейс состояния, который будут реализовывать все классы состояний:

<?php
interface OrderState {
    public function proceed(Order $order): string;
    public function getStatus(): string;
}

Создайте конкретные классы состояний, реализующие интерфейс:

<?php
class PendingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ProcessingState());
        return "Order moved to processing";
    }
    
    public function getStatus(): string {
        return "Pending";
    }
}

class ProcessingState implements OrderState {
    public function proceed(Order $order): string {
        $order->setState(new ShippedState());
        return "Order has been shipped";
    }
    
    public function getStatus(): string {
        return "Processing";
    }
}

class ShippedState implements OrderState {
    public function proceed(Order $order): string {
        return "Order already shipped";
    }
    
    public function getStatus(): string {
        return "Shipped";
    }
}

Создайте класс контекста, который хранит ссылку на текущее состояние и делегирует ему вызовы:

<?php
class Order {
    private OrderState $state;
    
    public function __construct() {
        $this->state = new PendingState();
    }
    
    public function setState(OrderState $state): void {
        $this->state = $state;
    }
    
    public function proceed(): string {
        return $this->state->proceed($this);
    }
    
    public function getStatus(): string {
        return $this->state->getStatus();
    }
}

Пример использования:

$order = new Order();
echo $order->getStatus() . "\n";  // Ожидание (Pending)
echo $order->proceed() . "\n";     // Заказ переведен в обработку (Order moved to processing)
echo $order->getStatus() . "\n";   // Обработка (Processing)
echo $order->proceed() . "\n";     // Заказ был отправлен (Order has been shipped)
echo $order->getStatus();          // Отправлен (Shipped)

Паттерн Состояние избавляет от сложных операторов switch и делает добавление новых состояний простым процессом. Каждый класс состояния отвечает за знание того, какое состояние следует за ним.

Попробуйте сами

<?php

require_once 'Document.php';

// Считать ввод - количество вызовов publish()
$n = intval(trim(fgets(STDIN)));

// TODO: Создать Document с заголовком "My Article"

// TODO: Вывести начальный статус в формате: "Status: [status]"

// TODO: Вызвать publish() n раз, выводя каждый результат на новой строке

// TODO: Вывести конечный статус в формате: "Status: [status]"

?>
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование