Перейти к содержанию

Состояние

Также известен как: State

Состояние — это поведенческий паттерн проектирования, который позволяет объектам менять поведение в зависимости от своего состояния. Извне создаётся впечатление, что изменился класс объекта.

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

Проблема

Паттерн Состояние невозможно рассматривать в отрыве от концепции машины состояний, также известной как стейт-машина или конечный автомат.

Конечный автомат

Конечный автомат.

Основная идея в том, что программа может находиться в одном из нескольких состояний, которые всё время сменяют друг друга. Набор этих состояний, а также переходов между ними, предопределён и конечен. Находясь в разных состояниях, программа может по-разному реагировать на одни и те же события, которые происходят с ней.

Такой подход можно применить и к отдельным объектам. Например, объект Документ может принимать три состояния: Черновик, Модерация или Опубликован. В каждом из этих состоянии метод опубликовать будет работать по-разному:

  • Из черновика он отправит документ на модерацию.
  • Из модерации — в публикацию, но при условии, что это сделал администратор.
  • В опубликованном состоянии метод не будет делать ничего.

Возможные состояния документа и переходы между ними

Возможные состояния документа и переходы между ними.

Машину состояний чаще всего реализуют с помощью множества условных операторов, if либо switch, которые проверяют текущее состояние объекта и выполняют соответствующее поведение. Наверняка вы уже реализовали хотя бы одну машину состояний в своей жизни, даже не зная об этом. Как насчёт вот такого кода, выглядит знакомо?

class Document is
    field state: string
    // ...
    method publish() is
        switch (state)
            "draft":
                state = "moderation"
                break
            "moderation":
                if (currentUser.role == "admin")
                    state = "published"
                break
            "published":
                // Do nothing.
                break
    // ...

Основная проблема такой машины состояний проявится в том случае, если в Документ добавить ещё десяток состояний. Каждый метод будет состоять из увесистого условного оператора, перебирающего доступные состояния. Такой код крайне сложно поддерживать. Малейшее изменение логики переходов заставит вас перепроверять работу всех методов, которые содержат условные операторы машины состояний.

Путаница и нагромождение условий особенно сильно проявляется в старых проектах. Набор возможных состояний бывает трудно предопределить заранее, поэтому они всё время добавляются в процессе эволюции программы. Из-за этого решение, которое выглядело простым и эффективным в самом начале разработки, может впоследствии стать проекцией большого макаронного монстра.

Решение

Паттерн Состояние предлагает создать отдельные классы для каждого состояния, в котором может пребывать объект, а затем вынести туда поведения, соответствующие этим состояниям.

Вместо того, чтобы хранить код всех состояний, первоначальный объект, называемый контекстом, будет содержать ссылку на один из объектов-состояний и делегировать ему работу, зависящую от состояния.

Документ делегирует работу своему активному объекту-состоянию

Документ делегирует работу своему активному объекту-состоянию.

Благодаря тому, что объекты состояний будут иметь общий интерфейс, контекст сможет делегировать работу состоянию, не привязываясь к его классу. Поведение контекста можно будет изменить в любой момент, подключив к нему другой объект-состояние.

Очень важным нюансом, отличающим этот паттерн от Стратегии, является то, что и контекст, и сами конкретные состояния могут знать друг о друге и инициировать переходы от одного состояния к другому.

Аналогия из жизни

Ваш смартфон ведёт себя по-разному, в зависимости от текущего состояния:

  • Когда телефон разблокирован, нажатие кнопок телефона приводит к каким-то действиям.
  • Когда телефон заблокирован, нажатие кнопок приводит к экрану разблокировки.
  • Когда телефон разряжен, нажатие кнопок приводит к экрану зарядки.

Структура

Структура классов паттерна Состояние

  1. Контекст хранит ссылку на объект состояния и делегирует ему часть работы, зависящей от состояний. Контекст работает с этим объектом через общий интерфейс состояний. Контекст должен иметь метод для присваивания ему нового объекта-состояния.
  2. Состояние описывает общий интерфейс для всех конкретных состояний.
  3. Конкретные состояния реализуют поведения, связанные с определённым состоянием контекста. Иногда приходится создавать целые иерархии классов состояний, чтобы обобщить дублирующий код.
    Состояние может иметь обратную ссылку на объект контекста. Через неё не только удобно получать из контекста нужную информацию, но и осуществлять смену его состояния.
  4. И контекст, и объекты конкретных состояний могут решать, когда и какое следующее состояние будет выбрано. Чтобы переключить состояние, нужно подать другой объект-состояние в контекст.

Псевдокод

В этом примере паттерн Состояние изменяет функциональность одних и тех же элементов управления музыкальным проигрывателем, в зависимости от того, в каком состоянии находится сейчас проигрыватель.

Структура классов примера паттерна Состояние

Пример изменение поведения проигрывателя с помощью состояний.

Объект проигрывателя содержит объект-состояние, которому и делегирует основную работу. Изменяя состояния, можно менять то, как ведут себя элементы управления проигрывателя.

// Общий интерфейс всех состояний.
abstract class State is
    protected field player: AudioPlayer

    // Контекст передаёт себя в конструктор состояния, чтобы
    // состояние могло обращаться к его данным и методам в
    // будущем, если потребуется.
    constructor State(player) is
        this.player = player

    abstract method clickLock()
    abstract method clickPlay()
    abstract method clickNext()
    abstract method clickPrevious()


// Конкретные состояния реализуют методы абстрактного состояния
// по-своему.
class LockedState extends State is

    // При разблокировке проигрователя с заблокированными
    // клавишами он может принять одно из двух состояний.
    method clickLock() is
        if (player.playing)
            player.changeState(new PlayingState(player))
        else
            player.changeState(new ReadyState(player))

    method clickPlay() is
        // Ничего не делать.

    method clickNext() is
        // Ничего не делать.

    method clickPrevious() is
        // Ничего не делать.


// Конкретные состояния сами могут переводить контекст в другое
// состояние.
class ReadyState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.startPlayback()
        player.changeState(new PlayingState(player))

    method clickNext() is
        player.nextSong()

    method clickPrevious() is
        player.previousSong()


class PlayingState extends State is
    method clickLock() is
        player.changeState(new LockedState(player))

    method clickPlay() is
        player.stopPlayback()
        player.changeState(new ReadyState(player))

    method clickNext() is
        if (event.doubleclick)
            player.nextSong()
        else
            player.fastForward(5)

    method clickPrevious() is
        if (event.doubleclick)
            player.previous()
        else
            player.rewind(5)


// Проигрыватель выступает в роли контекста.
class AudioPlayer is
    field state: State
    field UI, volume, playlist, currentSong

    constructor AudioPlayer() is
        this.state = new ReadyState(this)

        // Контекст заставляет состояние реагировать на
        // пользовательский ввод вместо себя. Реакция может быть
        // разной, в зависимости от того, какое состояние сейчас
        // активно.
        UI = new UserInterface()
        UI.lockButton.onClick(this.clickLock)
        UI.playButton.onClick(this.clickPlay)
        UI.nextButton.onClick(this.clickNext)
        UI.prevButton.onClick(this.clickPrevious)

    // Другие объекты тоже должны иметь возможность заменять
    // состояние проигрывателя.
    method changeState(state: State) is
        this.state = state

    // Методы UI будут делегировать работу активному состоянию.
    method clickLock() is
        state.clickLock()
    method clickPlay() is
        state.clickPlay()
    method clickNext() is
        state.clickNext()
    method clickPrevious() is
        state.clickPrevious()

    // Сервисные методы контекста, вызываемые состояниями.
    method startPlayback() is
        // ...
    method stopPlayback() is
        // ...
    method nextSong() is
        // ...
    method previousSong() is
        // ...
    method fastForward(time) is
        // ...
    method rewind(time) is
        // ...

Применимость

Когда у вас есть объект, поведение которого кардинально меняется в зависимости от внутреннего состояния, причём типов состояний много, и их код часто меняется.

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

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

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

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

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

Шаги реализации

  1. Определитесь с классом, который будет играть роль контекста. Это может быть как существующий класс, в котором уже есть зависимость от состояния, так и новый класс, если код состояний размазан по нескольким классам.
  2. Создайте общий интерфейс состояний. Он должен описывать методы, общие для всех состояний, обнаруженных в контексте. Заметьте, что не всё поведение контекста нужно переносить в состояние, а только то, которое зависит от состояний.
  3. Для каждого фактического состояния создайте класс, реализующий интерфейс состояния. Переместите код, связанный с конкретными состояниями в нужные классы. В конце концов, все методы интерфейса состояния должны быть реализованы во всех классах состояний.
    При переносе поведения из контекста вы можете столкнуться с тем, что это поведение зависит от приватных полей или методов контекста, к которым нет доступа из объекта состояния. Существует парочка способов обойти эту проблему.
    Самый простой — оставить поведение внутри контекста, вызывая его из объекта состояния. С другой стороны, вы можете сделать классы состояний вложенными в класс контекста, и тогда они получат доступ ко всем приватным частям контекста. Но последний способ доступен только в некоторых языках программирования (например, Java, C#).
  4. Создайте в контексте поле для хранения объектов-состояний, а также публичный метод для изменения значения этого поля.
  5. Старые методы контекста, в которых находился зависимый от состояния код, замените на вызовы соответствующих методов объекта-состояния.
  6. В зависимости от бизнес-логики, разместите код, который переключает состояние контекста либо внутри контекста, либо внутри классов конкретных состояний.

Преимущества и недостатки

Преимущества

  • Избавляет от множества больших условных операторов машины состояний.
  • Концентрирует в одном месте код, связанный с определённым состоянием.
  • Упрощает код контекста.

Недостатки

  • Может неоправданно усложнить код, если состояний мало и они редко меняются.

Примеры реализации паттерна

Python

  • Сложность: 1/3
  • Популярность: 2/3
  • Применимость: Паттерн Состояние часто используют в Python для превращения в объекты громоздких стейт-машин, построенных на операторах switch.
  • Признаки применения паттерна: Методы класса делегируют работу одному вложенному объекту.

Этот пример показывает структуру паттерна Состояние, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

from __future__ import annotations
from abc import ABC, abstractmethod


class Context:
    """
    Контекст определяет интерфейс, представляющий интерес для
    клиентов. Он также хранит ссылку на экземпляр подкласса Состояния,
    который отображает текущее состояние Контекста.
    """

    _state = None
    """
    Ссылка на текущее состояние Контекста.
    """

    def __init__(self, state: State) -> None:
        self.transition_to(state)

    def transition_to(self, state: State):
        """
        Контекст позволяет изменять объект Состояния во время
        выполнения.
        """

        print(f"Context: Transition to {type(state).__name__}")
        self._state = state
        self._state.context = self

    """
    Контекст делегирует часть своего поведения текущему объекту
    Состояния.
    """

    def request1(self):
        self._state.handle1()

    def request2(self):
        self._state.handle2()


class State(ABC):
    """
    Базовый класс Состояния объявляет методы, которые должны
    реализовать все Конкретные Состояния, а также предоставляет
    обратную ссылку на объект Контекст, связанный с Состоянием.
    Эта обратная ссылка может использоваться Состояниями для
    передачи Контекста другому Состоянию.
    """

    @property
    def context(self) -> Context:
        return self._context

    @context.setter
    def context(self, context: Context) -> None:
        self._context = context

    @abstractmethod
    def handle1(self) -> None:
        pass

    @abstractmethod
    def handle2(self) -> None:
        pass


"""
Конкретные Состояния реализуют различные модели поведения, связанные с
состоянием Контекста.
"""


class ConcreteStateA(State):
    def handle1(self) -> None:
        print("ConcreteStateA handles request1.")
        print("ConcreteStateA wants to change the state of the "
            + "context.")
        self.context.transition_to(ConcreteStateB())

    def handle2(self) -> None:
        print("ConcreteStateA handles request2.")


class ConcreteStateB(State):
    def handle1(self) -> None:
        print("ConcreteStateB handles request1.")

    def handle2(self) -> None:
        print("ConcreteStateB handles request2.")
        print("ConcreteStateB wants to change the state of the "
            + "context.")
        self.context.transition_to(ConcreteStateA())


if __name__ == "__main__":
    # Клиентский код.

    context = Context(ConcreteStateA())
    context.request1()
    context.request2()
Context: Transition to ConcreteStateA
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to ConcreteStateB
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to ConcreteStateA

PHP

  • Сложность: 1/3
  • Популярность: 1/3
  • Применимость: Паттерн Состояние иногда используют в PHP для превращения громоздких стейт-машин, построенных на операторах switch, в объекты.
  • Признаки применения паттерна: Методы класса делегируют работу одному вложенному объекту.

Этот пример показывает структуру паттерна Состояние, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.

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

<?php
/**
 * Контекст определяет интерфейс, представляющий интерес для клиентов.
 * Он также хранит ссылку на экземпляр подкласса Состояния, который
 * отображает текущее состояние Контекста.
 */
class Context
{
    /**
     * @var State Ссылка на текущее состояние Контекста.
     */
    private $state;

    public function __construct(State $state)
    {
        $this->transitionTo($state);
    }

    /**
     * Контекст позволяет изменять объект Состояния во время
     * выполнения.
     */
    public function transitionTo(State $state): void
    {
        echo "Context: Transition to " . get_class($state) . ".\n";
        $this->state = $state;
        $this->state->setContext($this);
    }

    /**
     * Контекст делегирует часть своего поведения текущему объекту
     * Состояния.
     */
    public function request1(): void
    {
        $this->state->handle1();
    }

    public function request2(): void
    {
        $this->state->handle2();
    }
}

/**
 * Базовый класс Состояния объявляет методы, которые должны
 * реализовать все Конкретные Состояния, а также предоставляет
 * обратную ссылку на объект Контекст, связанный с Состоянием.
 * Эта обратная ссылка может использоваться Состояниями 
 * для передачи Контекста другому Состоянию.
 */
abstract class State
{
    /**
     * @var Context
     */
    protected $context;

    public function setContext(Context $context)
    {
        $this->context = $context;
    }

    abstract public function handle1(): void;

    abstract public function handle2(): void;
}

/**
 * Конкретные Состояния реализуют различные модели поведения,
 * связанные с состоянием Контекста.
 */
class ConcreteStateA extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateA handles request1.\n";
        echo "ConcreteStateA wants to change the state of the "
            . "context.\n";
        $this->context->transitionTo(new ConcreteStateB());
    }

    public function handle2(): void
    {
        echo "ConcreteStateA handles request2.\n";
    }
}

class ConcreteStateB extends State
{
    public function handle1(): void
    {
        echo "ConcreteStateB handles request1.\n";
    }

    public function handle2(): void
    {
        echo "ConcreteStateB handles request2.\n";
        echo "ConcreteStateB wants to change the state of the "
            . "context.\n";
        $this->context->transitionTo(new ConcreteStateA());
    }
}

/**
 * Клиентский код.
 */
$context = new Context(new ConcreteStateA());
$context->request1();
$context->request2();
Context: Transition to RefactoringGuru\State\Conceptual\
ConcreteStateA.
ConcreteStateA handles request1.
ConcreteStateA wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\
ConcreteStateB.
ConcreteStateB handles request2.
ConcreteStateB wants to change the state of the context.
Context: Transition to RefactoringGuru\State\Conceptual\
ConcreteStateA.

Задачи

Задача 1. Светофор

Реализуйте светофор как конечный автомат с состояниями Red, Yellow, Green. Каждое состояние знает, в какое следующее переходить. Контекст TrafficLight делегирует переключение активному состоянию. Добавьте таймер: каждое состояние хранит длительность (в секундах).

from __future__ import annotations
from abc import ABC, abstractmethod


class TrafficLightState(ABC):
    def __init__(self, light: "TrafficLight") -> None:
        self._light = light

    @property
    @abstractmethod
    def color(self) -> str: ...

    @property
    @abstractmethod
    def duration(self) -> int: ...

    @abstractmethod
    def next(self) -> None:
        """Перейти в следующее состояние."""
        ...


class RedState(TrafficLightState):
    @property
    def color(self) -> str:
        return "RED"

    @property
    def duration(self) -> int:
        return 60

    def next(self) -> None:
        # TODO: перейти в GreenState
        pass


class YellowState(TrafficLightState):
    @property
    def color(self) -> str:
        return "YELLOW"

    @property
    def duration(self) -> int:
        return 5

    def next(self) -> None:
        # TODO: перейти в RedState
        pass


class GreenState(TrafficLightState):
    @property
    def color(self) -> str:
        return "GREEN"

    @property
    def duration(self) -> int:
        return 45

    def next(self) -> None:
        # TODO: перейти в YellowState
        pass


class TrafficLight:
    def __init__(self) -> None:
        # TODO: начальное состояние — Red
        self._state: TrafficLightState | None = None

    def change_state(self, state: TrafficLightState) -> None:
        self._state = state
        print(f"[TrafficLight] → {state.color} ({state.duration}s)")

    def tick(self) -> None:
        """Переключить на следующее состояние."""
        # TODO: делегировать текущему состоянию
        pass

    @property
    def current_color(self) -> str:
        return self._state.color


# Ожидаемый результат:
# light = TrafficLight()
# # [TrafficLight] → RED (60s)
# light.tick()
# # [TrafficLight] → GREEN (45s)
# light.tick()
# # [TrafficLight] → YELLOW (5s)
# light.tick()
# # [TrafficLight] → RED (60s)
# print(light.current_color)  # RED
<?php
abstract class TrafficLightState
{
    public function __construct(protected TrafficLight $light) {}

    abstract public function getColor(): string;
    abstract public function getDuration(): int;
    abstract public function next(): void;
}

class RedState extends TrafficLightState
{
    public function getColor(): string { return "RED"; }
    public function getDuration(): int { return 60; }

    public function next(): void
    {
        // TODO: перейти в GreenState
    }
}

class YellowState extends TrafficLightState
{
    public function getColor(): string { return "YELLOW"; }
    public function getDuration(): int { return 5; }

    public function next(): void
    {
        // TODO: перейти в RedState
    }
}

class GreenState extends TrafficLightState
{
    public function getColor(): string { return "GREEN"; }
    public function getDuration(): int { return 45; }

    public function next(): void
    {
        // TODO: перейти в YellowState
    }
}

class TrafficLight
{
    private ?TrafficLightState $state = null;

    public function __construct()
    {
        // TODO: начальное состояние — Red
    }

    public function changeState(TrafficLightState $state): void
    {
        $this->state = $state;
        echo "[TrafficLight] → {$state->getColor()} ({$state->getDuration()}s)\n";
    }

    public function tick(): void
    {
        // TODO: делегировать текущему состоянию
    }

    public function getCurrentColor(): string
    {
        return $this->state->getColor();
    }
}

// Ожидаемый результат:
// $light = new TrafficLight();
// // [TrafficLight] → RED (60s)
// $light->tick();
// // [TrafficLight] → GREEN (45s)
// $light->tick();
// // [TrafficLight] → YELLOW (5s)
// $light->tick();
// // [TrafficLight] → RED (60s)
// echo $light->getCurrentColor();  // RED

Задача 2. Жизненный цикл заказа в интернет-магазине

Реализуйте систему управления заказом с состояниями: New → Paid → Shipped → Delivered → Completed. Из любого состояния возможен переход в Cancelled (кроме Delivered и Completed). Каждое состояние разрешает или запрещает определённые действия.

from __future__ import annotations
from abc import ABC, abstractmethod


class OrderState(ABC):
    def __init__(self, order: "Order") -> None:
        self._order = order

    @property
    @abstractmethod
    def name(self) -> str: ...

    def pay(self) -> None:
        print(f"[{self.name}] Действие недоступно в текущем состоянии")

    def ship(self) -> None:
        print(f"[{self.name}] Действие недоступно в текущем состоянии")

    def deliver(self) -> None:
        print(f"[{self.name}] Действие недоступно в текущем состоянии")

    def complete(self) -> None:
        print(f"[{self.name}] Действие недоступно в текущем состоянии")

    def cancel(self) -> None:
        print(f"[{self.name}] Действие недоступно в текущем состоянии")


class NewState(OrderState):
    @property
    def name(self) -> str:
        return "New"

    def pay(self) -> None:
        # TODO: перейти в PaidState
        pass

    def cancel(self) -> None:
        # TODO: перейти в CancelledState
        pass


class PaidState(OrderState):
    @property
    def name(self) -> str:
        return "Paid"

    def ship(self) -> None:
        # TODO: перейти в ShippedState
        pass

    def cancel(self) -> None:
        # TODO: перейти в CancelledState (возврат средств)
        pass


class ShippedState(OrderState):
    @property
    def name(self) -> str:
        return "Shipped"

    def deliver(self) -> None:
        # TODO: перейти в DeliveredState
        pass

    def cancel(self) -> None:
        # TODO: перейти в CancelledState
        pass


class DeliveredState(OrderState):
    @property
    def name(self) -> str:
        return "Delivered"

    def complete(self) -> None:
        # TODO: перейти в CompletedState
        pass

    # cancel недоступен из Delivered


class CompletedState(OrderState):
    @property
    def name(self) -> str:
        return "Completed"

    # Терминальное состояние — все действия недоступны


class CancelledState(OrderState):
    @property
    def name(self) -> str:
        return "Cancelled"

    # Терминальное состояние


class Order:
    def __init__(self, order_id: str) -> None:
        self.order_id = order_id
        self._state: OrderState = NewState(self)
        print(f"Order {order_id} создан. Статус: {self._state.name}")

    def change_state(self, state: OrderState) -> None:
        self._state = state
        print(f"Order {self.order_id} → {state.name}")

    def pay(self) -> None: self._state.pay()
    def ship(self) -> None: self._state.ship()
    def deliver(self) -> None: self._state.deliver()
    def complete(self) -> None: self._state.complete()
    def cancel(self) -> None: self._state.cancel()

    @property
    def status(self) -> str:
        return self._state.name


# Ожидаемый результат:
# order = Order("ORD-001")
# order.ship()     # [New] Действие недоступно
# order.pay()      # Order ORD-001 → Paid
# order.ship()     # Order ORD-001 → Shipped
# order.deliver()  # Order ORD-001 → Delivered
# order.cancel()   # [Delivered] Действие недоступно
# order.complete() # Order ORD-001 → Completed
# print(order.status)  # Completed
<?php
abstract class OrderState
{
    public function __construct(protected Order $order) {}

    abstract public function getName(): string;

    public function pay(): void { echo "[{$this->getName()}] Действие недоступно\n"; }
    public function ship(): void { echo "[{$this->getName()}] Действие недоступно\n"; }
    public function deliver(): void { echo "[{$this->getName()}] Действие недоступно\n"; }
    public function complete(): void { echo "[{$this->getName()}] Действие недоступно\n"; }
    public function cancel(): void { echo "[{$this->getName()}] Действие недоступно\n"; }
}

class NewState extends OrderState
{
    public function getName(): string { return "New"; }
    public function pay(): void { /* TODO: → PaidState */ }
    public function cancel(): void { /* TODO: → CancelledState */ }
}

class PaidState extends OrderState
{
    public function getName(): string { return "Paid"; }
    public function ship(): void { /* TODO: → ShippedState */ }
    public function cancel(): void { /* TODO: → CancelledState */ }
}

class ShippedState extends OrderState
{
    public function getName(): string { return "Shipped"; }
    public function deliver(): void { /* TODO: → DeliveredState */ }
    public function cancel(): void { /* TODO: → CancelledState */ }
}

class DeliveredState extends OrderState
{
    public function getName(): string { return "Delivered"; }
    public function complete(): void { /* TODO: → CompletedState */ }
}

class CompletedState extends OrderState
{
    public function getName(): string { return "Completed"; }
}

class CancelledState extends OrderState
{
    public function getName(): string { return "Cancelled"; }
}

class Order
{
    private OrderState $state;

    public function __construct(private readonly string $orderId)
    {
        $this->state = new NewState($this);
        echo "Order $orderId создан. Статус: {$this->state->getName()}\n";
    }

    public function changeState(OrderState $state): void
    {
        $this->state = $state;
        echo "Order {$this->orderId} → {$state->getName()}\n";
    }

    public function pay(): void { $this->state->pay(); }
    public function ship(): void { $this->state->ship(); }
    public function deliver(): void { $this->state->deliver(); }
    public function complete(): void { $this->state->complete(); }
    public function cancel(): void { $this->state->cancel(); }
    public function getStatus(): string { return $this->state->getName(); }
}

// Ожидаемый результат:
// $order = new Order("ORD-001");
// $order->ship();     // [New] Действие недоступно
// $order->pay();      // Order ORD-001 → Paid
// $order->ship();     // Order ORD-001 → Shipped
// $order->deliver();  // Order ORD-001 → Delivered
// $order->cancel();   // [Delivered] Действие недоступно
// $order->complete(); // Order ORD-001 → Completed
// echo $order->getStatus();  // Completed

Задача 3. Банкомат (ATM)

Реализуйте банкомат с состояниями: Idle (ожидание карты), CardInserted (карта вставлена, ожидание PIN), Authenticated (PIN введён, выбор операции), DispensingCash (выдача наличных). Неверный PIN трижды — переход в CardBlocked.

from __future__ import annotations
from abc import ABC, abstractmethod


class ATMState(ABC):
    def __init__(self, atm: "ATM") -> None:
        self._atm = atm

    @abstractmethod
    def insert_card(self, card_number: str) -> None: ...

    @abstractmethod
    def enter_pin(self, pin: str) -> None: ...

    @abstractmethod
    def select_amount(self, amount: float) -> None: ...

    @abstractmethod
    def eject_card(self) -> None: ...


class IdleState(ATMState):
    def insert_card(self, card_number: str) -> None:
        # TODO: сохранить card_number в ATM, перейти в CardInsertedState
        pass

    def enter_pin(self, pin: str) -> None:
        print("[Idle] Сначала вставьте карту")

    def select_amount(self, amount: float) -> None:
        print("[Idle] Сначала вставьте карту")

    def eject_card(self) -> None:
        print("[Idle] Карта не вставлена")


class CardInsertedState(ATMState):
    def insert_card(self, card_number: str) -> None:
        print("[CardInserted] Карта уже вставлена")

    def enter_pin(self, pin: str) -> None:
        # TODO: проверить PIN через atm.verify_pin(pin),
        #       при успехе → AuthenticatedState,
        #       при 3 неудачах → CardBlockedState
        pass

    def select_amount(self, amount: float) -> None:
        print("[CardInserted] Сначала введите PIN")

    def eject_card(self) -> None:
        # TODO: вернуть карту, → IdleState
        pass


class AuthenticatedState(ATMState):
    def insert_card(self, card_number: str) -> None:
        print("[Authenticated] Сессия уже активна")

    def enter_pin(self, pin: str) -> None:
        print("[Authenticated] PIN уже введён")

    def select_amount(self, amount: float) -> None:
        # TODO: проверить баланс, перейти в DispensingCashState
        pass

    def eject_card(self) -> None:
        # TODO: завершить сессию, → IdleState
        pass


class DispensingCashState(ATMState):
    def insert_card(self, card_number: str) -> None:
        print("[Dispensing] Операция выполняется")

    def enter_pin(self, pin: str) -> None:
        print("[Dispensing] Операция выполняется")

    def select_amount(self, amount: float) -> None:
        print("[Dispensing] Операция выполняется")

    def eject_card(self) -> None:
        # TODO: выдать наличные, списать с баланса, → IdleState
        pass


class CardBlockedState(ATMState):
    def insert_card(self, card_number: str) -> None:
        print("[Blocked] Карта заблокирована")

    def enter_pin(self, pin: str) -> None:
        print("[Blocked] Карта заблокирована")

    def select_amount(self, amount: float) -> None:
        print("[Blocked] Карта заблокирована")

    def eject_card(self) -> None:
        # TODO: вернуть карту (заблокированную), → IdleState
        pass


class ATM:
    def __init__(self, balance: float = 10000.0) -> None:
        self._balance = balance
        self._card_number: str | None = None
        self._pin_attempts = 0
        self._requested_amount = 0.0
        self._state: ATMState = IdleState(self)
        # Фиктивная БД: card_number → PIN
        self._cards = {"1234-5678": "1234", "8765-4321": "0000"}
        self._accounts = {"1234-5678": 5000.0, "8765-4321": 300.0}

    def change_state(self, state: ATMState) -> None:
        self._state = state

    def verify_pin(self, pin: str) -> bool:
        return self._cards.get(self._card_number) == pin

    def get_account_balance(self) -> float:
        return self._accounts.get(self._card_number, 0.0)

    def deduct(self, amount: float) -> None:
        self._accounts[self._card_number] -= amount
        self._balance -= amount

    # Публичный интерфейс
    def insert_card(self, card_number: str) -> None:
        self._state.insert_card(card_number)

    def enter_pin(self, pin: str) -> None:
        self._state.enter_pin(pin)

    def select_amount(self, amount: float) -> None:
        self._state.select_amount(amount)

    def eject_card(self) -> None:
        self._state.eject_card()


# Ожидаемый результат:
# atm = ATM()
# atm.insert_card("1234-5678")   # Карта принята
# atm.enter_pin("9999")          # Неверный PIN (попытка 1/3)
# atm.enter_pin("9999")          # Неверный PIN (попытка 2/3)
# atm.enter_pin("1234")          # Успешная аутентификация
# atm.select_amount(500.0)       # Запрос 500.0
# atm.eject_card()               # Выдано 500.0, карта возвращена
<?php
abstract class ATMState
{
    public function __construct(protected ATM $atm) {}

    abstract public function insertCard(string $cardNumber): void;
    abstract public function enterPin(string $pin): void;
    abstract public function selectAmount(float $amount): void;
    abstract public function ejectCard(): void;
}

class IdleState extends ATMState
{
    public function insertCard(string $cardNumber): void
    { /* TODO: сохранить карту, → CardInsertedState */ }

    public function enterPin(string $pin): void
    { echo "[Idle] Сначала вставьте карту\n"; }

    public function selectAmount(float $amount): void
    { echo "[Idle] Сначала вставьте карту\n"; }

    public function ejectCard(): void
    { echo "[Idle] Карта не вставлена\n"; }
}

class CardInsertedState extends ATMState
{
    public function insertCard(string $cardNumber): void
    { echo "[CardInserted] Карта уже вставлена\n"; }

    public function enterPin(string $pin): void
    { /* TODO: проверить PIN, при 3 неудачах → CardBlockedState */ }

    public function selectAmount(float $amount): void
    { echo "[CardInserted] Сначала введите PIN\n"; }

    public function ejectCard(): void
    { /* TODO: вернуть карту, → IdleState */ }
}

class AuthenticatedState extends ATMState
{
    public function insertCard(string $cardNumber): void
    { echo "[Authenticated] Сессия уже активна\n"; }

    public function enterPin(string $pin): void
    { echo "[Authenticated] PIN уже введён\n"; }

    public function selectAmount(float $amount): void
    { /* TODO: проверить баланс, → DispensingCashState */ }

    public function ejectCard(): void
    { /* TODO: завершить сессию, → IdleState */ }
}

class DispensingCashState extends ATMState
{
    public function insertCard(string $cardNumber): void { echo "[Dispensing] Операция выполняется\n"; }
    public function enterPin(string $pin): void { echo "[Dispensing] Операция выполняется\n"; }
    public function selectAmount(float $amount): void { echo "[Dispensing] Операция выполняется\n"; }

    public function ejectCard(): void
    { /* TODO: выдать наличные, списать с баланса, → IdleState */ }
}

class CardBlockedState extends ATMState
{
    public function insertCard(string $cardNumber): void { echo "[Blocked] Карта заблокирована\n"; }
    public function enterPin(string $pin): void { echo "[Blocked] Карта заблокирована\n"; }
    public function selectAmount(float $amount): void { echo "[Blocked] Карта заблокирована\n"; }

    public function ejectCard(): void
    { /* TODO: вернуть карту, → IdleState */ }
}

class ATM
{
    private ATMState $state;
    public ?string $cardNumber = null;
    public int $pinAttempts = 0;
    public float $requestedAmount = 0.0;
    private array $cards = ["1234-5678" => "1234", "8765-4321" => "0000"];
    private array $accounts = ["1234-5678" => 5000.0, "8765-4321" => 300.0];

    public function __construct(private float $balance = 10000.0)
    {
        $this->state = new IdleState($this);
    }

    public function changeState(ATMState $state): void { $this->state = $state; }
    public function verifyPin(string $pin): bool { return ($this->cards[$this->cardNumber] ?? null) === $pin; }
    public function getAccountBalance(): float { return $this->accounts[$this->cardNumber] ?? 0.0; }
    public function deduct(float $amount): void { $this->accounts[$this->cardNumber] -= $amount; $this->balance -= $amount; }

    public function insertCard(string $cardNumber): void { $this->state->insertCard($cardNumber); }
    public function enterPin(string $pin): void { $this->state->enterPin($pin); }
    public function selectAmount(float $amount): void { $this->state->selectAmount($amount); }
    public function ejectCard(): void { $this->state->ejectCard(); }
}

// Ожидаемый результат:
// $atm = new ATM();
// $atm->insertCard("1234-5678");   // Карта принята
// $atm->enterPin("9999");          // Неверный PIN (попытка 1/3)
// $atm->enterPin("9999");          // Неверный PIN (попытка 2/3)
// $atm->enterPin("1234");          // Успешная аутентификация
// $atm->selectAmount(500.0);       // Запрос 500.0
// $atm->ejectCard();               // Выдано 500.0, карта возвращена

Задача 4. TCP-соединение

Реализуйте модель TCP-соединения с состояниями: Closed, Listen, SynReceived, Established. Переходы: Closed → Listen (open), Listen → SynReceived (syn), SynReceived → Established (ack), Established → Closed (close). В состоянии Established доступна отправка данных.

from __future__ import annotations
from abc import ABC, abstractmethod


class TCPState(ABC):
    def __init__(self, connection: "TCPConnection") -> None:
        self._conn = connection

    @property
    @abstractmethod
    def name(self) -> str: ...

    def open(self) -> None:
        print(f"[{self.name}] Недопустимое действие")

    def syn(self) -> None:
        print(f"[{self.name}] Недопустимое действие")

    def ack(self) -> None:
        print(f"[{self.name}] Недопустимое действие")

    def close(self) -> None:
        print(f"[{self.name}] Недопустимое действие")

    def send(self, data: str) -> None:
        print(f"[{self.name}] Соединение не установлено")


class ClosedState(TCPState):
    @property
    def name(self) -> str:
        return "CLOSED"

    def open(self) -> None:
        # TODO: → ListenState
        pass


class ListenState(TCPState):
    @property
    def name(self) -> str:
        return "LISTEN"

    def syn(self) -> None:
        # TODO: → SynReceivedState
        pass

    def close(self) -> None:
        # TODO: → ClosedState
        pass


class SynReceivedState(TCPState):
    @property
    def name(self) -> str:
        return "SYN_RECEIVED"

    def ack(self) -> None:
        # TODO: → EstablishedState
        pass

    def close(self) -> None:
        # TODO: → ClosedState
        pass


class EstablishedState(TCPState):
    @property
    def name(self) -> str:
        return "ESTABLISHED"

    def send(self, data: str) -> None:
        print(f"[ESTABLISHED] Отправка данных: '{data}'")
        self._conn.log_sent(data)

    def close(self) -> None:
        # TODO: → ClosedState, выполнить four-way handshake (FIN)
        pass


class TCPConnection:
    def __init__(self) -> None:
        self._state: TCPState = ClosedState(self)
        self._sent: list[str] = []
        print(f"TCP Connection создано. Состояние: {self._state.name}")

    def change_state(self, state: TCPState) -> None:
        self._state = state
        print(f"TCP → {state.name}")

    def log_sent(self, data: str) -> None:
        self._sent.append(data)

    def open(self) -> None: self._state.open()
    def syn(self) -> None: self._state.syn()
    def ack(self) -> None: self._state.ack()
    def close(self) -> None: self._state.close()
    def send(self, data: str) -> None: self._state.send(data)

    @property
    def state_name(self) -> str:
        return self._state.name


# Ожидаемый результат:
# conn = TCPConnection()   # CLOSED
# conn.send("hello")       # [CLOSED] Соединение не установлено
# conn.open()              # TCP → LISTEN
# conn.syn()               # TCP → SYN_RECEIVED
# conn.ack()               # TCP → ESTABLISHED
# conn.send("Hello!")      # [ESTABLISHED] Отправка данных: 'Hello!'
# conn.send("World!")      # [ESTABLISHED] Отправка данных: 'World!'
# conn.close()             # TCP → CLOSED
# conn.send("late")        # [CLOSED] Соединение не установлено
<?php
abstract class TCPState
{
    public function __construct(protected TCPConnection $conn) {}

    abstract public function getName(): string;

    public function open(): void { echo "[{$this->getName()}] Недопустимое действие\n"; }
    public function syn(): void { echo "[{$this->getName()}] Недопустимое действие\n"; }
    public function ack(): void { echo "[{$this->getName()}] Недопустимое действие\n"; }
    public function close(): void { echo "[{$this->getName()}] Недопустимое действие\n"; }
    public function send(string $data): void { echo "[{$this->getName()}] Соединение не установлено\n"; }
}

class ClosedState extends TCPState
{
    public function getName(): string { return "CLOSED"; }
    public function open(): void { /* TODO: → ListenState */ }
}

class ListenState extends TCPState
{
    public function getName(): string { return "LISTEN"; }
    public function syn(): void { /* TODO: → SynReceivedState */ }
    public function close(): void { /* TODO: → ClosedState */ }
}

class SynReceivedState extends TCPState
{
    public function getName(): string { return "SYN_RECEIVED"; }
    public function ack(): void { /* TODO: → EstablishedState */ }
    public function close(): void { /* TODO: → ClosedState */ }
}

class EstablishedState extends TCPState
{
    public function getName(): string { return "ESTABLISHED"; }

    public function send(string $data): void
    {
        echo "[ESTABLISHED] Отправка данных: '$data'\n";
        $this->conn->logSent($data);
    }

    public function close(): void { /* TODO: → ClosedState */ }
}

class TCPConnection
{
    private TCPState $state;
    private array $sent = [];

    public function __construct()
    {
        $this->state = new ClosedState($this);
        echo "TCP Connection создано. Состояние: {$this->state->getName()}\n";
    }

    public function changeState(TCPState $state): void
    {
        $this->state = $state;
        echo "TCP → {$state->getName()}\n";
    }

    public function logSent(string $data): void { $this->sent[] = $data; }

    public function open(): void { $this->state->open(); }
    public function syn(): void { $this->state->syn(); }
    public function ack(): void { $this->state->ack(); }
    public function close(): void { $this->state->close(); }
    public function send(string $data): void { $this->state->send($data); }
    public function getStateName(): string { return $this->state->getName(); }
}

// Ожидаемый результат:
// $conn = new TCPConnection();  // CLOSED
// $conn->send("hello");         // [CLOSED] Соединение не установлено
// $conn->open();                // TCP → LISTEN
// $conn->syn();                 // TCP → SYN_RECEIVED
// $conn->ack();                 // TCP → ESTABLISHED
// $conn->send("Hello!");        // [ESTABLISHED] Отправка данных: 'Hello!'
// $conn->close();               // TCP → CLOSED

Задача 5. Торговый автомат (Vending Machine)

Реализуйте торговый автомат с состояниями: Idle (ожидание монеты), HasMoney (монета внесена, выбор товара), Dispensing (выдача товара), OutOfStock (нет товаров). Поддержите: внесение нескольких монет, сдача при недостаточной сумме, возврат денег.

from __future__ import annotations
from abc import ABC, abstractmethod


class VendingState(ABC):
    def __init__(self, machine: "VendingMachine") -> None:
        self._machine = machine

    @abstractmethod
    def insert_coin(self, amount: float) -> None: ...

    @abstractmethod
    def select_product(self, product: str) -> None: ...

    @abstractmethod
    def cancel(self) -> None: ...

    @abstractmethod
    def dispense(self) -> None: ...


class IdleState(VendingState):
    def insert_coin(self, amount: float) -> None:
        # TODO: добавить деньги в machine.balance, → HasMoneyState
        pass

    def select_product(self, product: str) -> None:
        print("[Idle] Сначала внесите монету")

    def cancel(self) -> None:
        print("[Idle] Нечего отменять")

    def dispense(self) -> None:
        print("[Idle] Сначала выберите товар")


class HasMoneyState(VendingState):
    def insert_coin(self, amount: float) -> None:
        # TODO: добавить к текущему балансу
        pass

    def select_product(self, product: str) -> None:
        # TODO: проверить наличие товара и достаточность суммы,
        #       установить machine.selected_product, → DispensingState
        #       если недостаточно средств — сообщить и остаться
        pass

    def cancel(self) -> None:
        # TODO: вернуть деньги, → IdleState
        pass

    def dispense(self) -> None:
        print("[HasMoney] Сначала выберите товар")


class DispensingState(VendingState):
    def insert_coin(self, amount: float) -> None:
        print("[Dispensing] Подождите, идёт выдача")

    def select_product(self, product: str) -> None:
        print("[Dispensing] Подождите, идёт выдача")

    def cancel(self) -> None:
        print("[Dispensing] Невозможно отменить")

    def dispense(self) -> None:
        # TODO: выдать товар, вернуть сдачу,
        #       если товаров больше нет → OutOfStockState, иначе → IdleState
        pass


class OutOfStockState(VendingState):
    def insert_coin(self, amount: float) -> None:
        print("[OutOfStock] Нет товаров, монета возвращена")

    def select_product(self, product: str) -> None:
        print("[OutOfStock] Нет товаров")

    def cancel(self) -> None:
        print("[OutOfStock] Нет активной транзакции")

    def dispense(self) -> None:
        print("[OutOfStock] Нет товаров")


class VendingMachine:
    def __init__(self) -> None:
        self.balance: float = 0.0
        self.selected_product: str | None = None
        self._inventory: dict[str, tuple[float, int]] = {
            # product: (price, quantity)
            "Cola": (1.5, 3),
            "Water": (1.0, 2),
            "Chips": (2.0, 1),
        }
        self._state: VendingState = IdleState(self)

    def change_state(self, state: VendingState) -> None:
        self._state = state

    def get_price(self, product: str) -> float | None:
        item = self._inventory.get(product)
        return item[0] if item else None

    def get_stock(self, product: str) -> int:
        item = self._inventory.get(product)
        return item[1] if item else 0

    def reduce_stock(self, product: str) -> None:
        price, qty = self._inventory[product]
        self._inventory[product] = (price, qty - 1)

    def has_any_stock(self) -> bool:
        return any(qty > 0 for _, qty in self._inventory.values())

    def insert_coin(self, amount: float) -> None: self._state.insert_coin(amount)
    def select_product(self, product: str) -> None: self._state.select_product(product)
    def cancel(self) -> None: self._state.cancel()
    def dispense(self) -> None: self._state.dispense()


# Ожидаемый результат:
# vm = VendingMachine()
# vm.select_product("Cola")    # [Idle] Сначала внесите монету
# vm.insert_coin(1.0)          # Баланс: 1.0
# vm.insert_coin(1.0)          # Баланс: 2.0
# vm.select_product("Cola")    # Выбрана Cola (1.5). Сдача: 0.5
# vm.dispense()                # Выдаётся Cola. Сдача: 0.5 возвращена
# vm.insert_coin(1.0)
# vm.select_product("Water")
# vm.cancel()                  # Возврат 1.0
<?php
abstract class VendingState
{
    public function __construct(protected VendingMachine $machine) {}

    abstract public function insertCoin(float $amount): void;
    abstract public function selectProduct(string $product): void;
    abstract public function cancel(): void;
    abstract public function dispense(): void;
}

class IdleState extends VendingState
{
    public function insertCoin(float $amount): void
    { /* TODO: добавить к балансу, → HasMoneyState */ }

    public function selectProduct(string $product): void
    { echo "[Idle] Сначала внесите монету\n"; }

    public function cancel(): void { echo "[Idle] Нечего отменять\n"; }
    public function dispense(): void { echo "[Idle] Сначала выберите товар\n"; }
}

class HasMoneyState extends VendingState
{
    public function insertCoin(float $amount): void
    { /* TODO: добавить к балансу */ }

    public function selectProduct(string $product): void
    { /* TODO: проверить наличие и баланс, → DispensingState */ }

    public function cancel(): void
    { /* TODO: вернуть деньги, → IdleState */ }

    public function dispense(): void { echo "[HasMoney] Сначала выберите товар\n"; }
}

class DispensingState extends VendingState
{
    public function insertCoin(float $amount): void { echo "[Dispensing] Подождите\n"; }
    public function selectProduct(string $product): void { echo "[Dispensing] Подождите\n"; }
    public function cancel(): void { echo "[Dispensing] Невозможно отменить\n"; }

    public function dispense(): void
    { /* TODO: выдать товар, сдача, → IdleState или OutOfStockState */ }
}

class OutOfStockState extends VendingState
{
    public function insertCoin(float $amount): void { echo "[OutOfStock] Монета возвращена\n"; }
    public function selectProduct(string $product): void { echo "[OutOfStock] Нет товаров\n"; }
    public function cancel(): void { echo "[OutOfStock] Нет транзакции\n"; }
    public function dispense(): void { echo "[OutOfStock] Нет товаров\n"; }
}

class VendingMachine
{
    public float $balance = 0.0;
    public ?string $selectedProduct = null;
    private array $inventory = [
        "Cola"  => ["price" => 1.5, "qty" => 3],
        "Water" => ["price" => 1.0, "qty" => 2],
        "Chips" => ["price" => 2.0, "qty" => 1],
    ];
    private VendingState $state;

    public function __construct()
    {
        $this->state = new IdleState($this);
    }

    public function changeState(VendingState $state): void { $this->state = $state; }
    public function getPrice(string $product): ?float { return $this->inventory[$product]["price"] ?? null; }
    public function getStock(string $product): int { return $this->inventory[$product]["qty"] ?? 0; }
    public function reduceStock(string $product): void { $this->inventory[$product]["qty"]--; }
    public function hasAnyStock(): bool { return (bool) array_filter($this->inventory, fn($i) => $i["qty"] > 0); }

    public function insertCoin(float $amount): void { $this->state->insertCoin($amount); }
    public function selectProduct(string $product): void { $this->state->selectProduct($product); }
    public function cancel(): void { $this->state->cancel(); }
    public function dispense(): void { $this->state->dispense(); }
}

// Ожидаемый результат:
// $vm = new VendingMachine();
// $vm->selectProduct("Cola");    // [Idle] Сначала внесите монету
// $vm->insertCoin(1.0);          // Баланс: 1.0
// $vm->insertCoin(1.0);          // Баланс: 2.0
// $vm->selectProduct("Cola");    // Выбрана Cola (1.5). К оплате: 1.5
// $vm->dispense();               // Выдаётся Cola. Сдача: 0.5 возвращена
// $vm->insertCoin(1.0);
// $vm->cancel();                 // Возврат 1.0