Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Паттерн Состояние (State) PHP: Полиморфизм

Паттерн "Состояние" – яркий пример замены условных конструкций на полиморфизм подтипов. Он довольно широко используется и способен по-настоящему снизить сложность кода. Разберём его на примере поведения экранов телефонов.

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

Всего у телефона три базовых состояния:

  1. Телефон выключен. Экран не реагирует на прикосновения.
  2. Телефон включен, но экран выключен. Экран реагирует только на прикосновение (но не на смахивание) и включается.
  3. Телефон включен и экран тоже. Реакция на прикосновения и жесты зависит от активного приложения.

Смоделируем эту логику в классе, отвечающем за экран, и добавим туда два события: прикосновение (touch) и смахивание (swipe).

<?php

class MobileScreen 
{
    public function __construct()
    {
        // В самом начале телефон выключен
        $this->powerOn = false;
        $this->screenOn = false;
    }

    // Включение питания
    public function powerOn()
    {
        $this->powerOn = true;
    }

    // Прикосновение
    public function touch()
    {
        // Если питание выключено, то ничего не происходит
        if (!$this->powerOn) {
            return;
        }

        // Если экран был выключен, то его надо включить
        if (!$this->screenOn) {
            $this->screenOn = true;
        }

        // На событие должно реагировать текущее активное приложение
        $this->notify('touch');
    }

    // Смахивание
    public function swipe() {
        // Если выключено питание или экран, то ничего не происходит
        if (!$this->powerOn || !$this->screenOn) {
            return;
        }

        // На событие должно реагировать текущее активное приложение
        $this->notify('swipe');
    }
}

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

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

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

Явно выделенное состояние

Текущая реализация экрана опирается на флаги. В программировании так называют переменные содержащие булевы значения.

<?php

public function __construct()
{
    $this->powerOn = false;
    $this->screenOn = false;
}

Флаги часто (но не всегда!) - признак плохой архитектуры. Они имеют тенденцию множиться и пересекаться. Логика, завязанная на комбинации разных флагов, усложняет анализ кода:

<?php

if (!$this->powerOn || !$this->screenOn) {
    return;
}

Такой стиль программирования имеет свое название: "флаговое программирование". Так говорят про код, в котором трудно разобраться из-за наличия логики, завязанной на комбинацию флагов. А наличие флагов почти наверняка к этому приведет. Все дело в том, что количество состояний у систем, как правило, больше двух. То есть одного флага никогда не будет достаточно.

От флагов возможно уйти, введя явное состояние системы. В нашем примере несложно заметить, что состояний всего три:

  • Power Off: Питание отключено (а значит и экран выключен).
  • Screen Disabled: Экран выключен (но питание включено).
  • Screen On: Экран включен.

Следующий шаг, заменить флаги на одну переменную, которая хранит текущее состояние системы:

class MobileScreen
{
    public function __construct()
    {
        $this->stateName = 'powerOff';
    }

    public function powerOn()
    {
        $this->stateName = 'powerOn';
    }

    public function touch()
    {
        if ($this->stateName === 'powerOff') {
            return;
        }

        if ($this->stateName === 'screenDisabled') {
            $this->stateName = 'screenOn';
        }

        $this->notify('touch');
    }

    public function swipe()
    {
        if ($this->stateName !== 'screenOn') {
            return;
        }

        // На событие должно реагировать текущее активное приложение
        $this->notify('swipe');
    }
}

Главное что произошло в коде выше – пропали проверки на комбинацию флагов. Это не отменяет возможности проверок сразу по нескольким состояниям, но состояния системы понимать гораздо проще чем наборы флагов.

Классы Состояний

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

Экран, в свою очередь, избавится от всех проверок и начнет взаимодействовать с состояниями:

<?php
namespace App;

use App\states\PowerOffState;
use App\states\ScreenDisabledState;
use App\states\ScreenOnState;

class MobileScreen
{
    public function __construct()
    {
        // Начальное состояние
        // Внутрь передается текущий объект
        // Это нужно для смены состояний (примеры ниже)
        $this->state = new PowerOffState($this);
    }

    public function powerOn()
    {
        // Предыдущее состояние нас не волнует
        // Все данные хранятся в самом экране
        // Объекты-состояния не имеют своих данных
        $this->state = new ScreenDisabledState($this);
    }

    public function touch()
    {
        $this->state->touch();
    }

    public function swipe()
    {
        $this->state->swipe();
    }
}

// Обратите внимание что с точки зрения внешнего кода (пользователя экрана)
// ничего не изменилось.

Теперь экран не делает ровным счетом ничего. Весь его код — это инициализация начального состояния и передача управления текущему активному состоянию. Как же выглядят классы состояний?

<?php

class PowerOffState
{
    public function __construct($screen)
    {
        $this->screen = $screen;
    }

    public function touch()
    {
        // ничего не происходит
    }

    public function swipe()
    {
        // ничего не происходит
    }
}

Проще всех устроено состояние выключенного телефона. В этом состоянии нет никакой реакции, поэтому методы пустые. Посмотрим ScreenDisabledState:

<?php

use App\states\ScreenOnState;

class ScreenDisabledState
{
    public function __construct($screen)
    {
        $this->screen = $screen;
    }

    public function touch()
    {
        // Включаем экран. В конструктор нужно передать сам экран.
        $this->screen->state = new ScreenOnState($this->screen);
        // Оповещаем текущую программу об активации
        $this->screen->notify('touch');
    }

    public function swipe()
    {
        // ничего не происходит
    }
}

Прикосновение к экрану оживляет его. Для этого состояние ScreenDisabledState должно выполнить переход в состояние ScreenOnState. Именно поэтому внутрь каждого состояния передавался сам экран. Иначе невозможно было бы его изменять.

И последнее состояние ScreenOnState. Это единственное состояние, в котором происходит взаимодействие с программами

<?php

class ScreenOnState
{
    public function __construct($screen)
    {
        $this->screen = $screen;
    }

    public function touch()
    {
        $this->screen->notify('touch');
    }

    public function swipe()
    {
        $this->screen->notify('swipe');
    }
}

Это невероятно, но в коде больше не осталось ни одной условной конструкции. Стало легко видеть поведение телефона на все события в конкретном состоянии. Достаточно открыть нужный класс. Цена за такое удобство – большее количество файлов и кода.

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

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

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы PHP-разработчик
Профессия
Разработка веб-приложений на Laravel
25 мая 10 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»