Зарегистрируйтесь, чтобы продолжить обучение

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

При разработке объект может находиться в различных состояниях и его поведение должно изменяться соответственно. Использование условных конструкций для управления поведением объекта в различных состояниях может привести к сложному и хрупкому коду. Паттерн «Состояние» позволяет заменить условные конструкции на полиморфизм подтипов. Это упрощает код и делает его более понятным и поддерживаемым.

В этом уроке мы изучим паттерн проектирования «Состояние» (State), который является инструментом для управления поведением объектов в зависимости от их состояния.

Уход от флагов к состояниям

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

Рассмотрим этот паттерн на примере поведения экранов мобильных телефонов. Мы будем считать, что у телефона может быть три базовых состояния:

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

Попробуем смоделировать эту логику в коде:

class MobileScreen:
    def __init__(self):
        # В самом начале телефон выключен
        self.power_on = False
        self.screen_on = False

    # Включение питания
    def power_on(self):
        self.power_on = True

    # Прикосновение
    def touch(self):
        # Если питание выключено, то ничего не происходит
        if not self.power_on:
            return

        # Если экран был выключен, то его надо включить
        if not self.screen_on:
            self.screen_on = True

        # На событие должно реагировать текущее активное приложение
        self.notify("touch")

    # Смахивание
    def swipe(self):
        # Если выключено питание или экран, то ничего не происходит
        if not self.power_on or not self.screen_on:
            return

        # На событие должно реагировать текущее активное приложение
        self.notify("swipe")

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

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

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

Уход от флагов к состояниям

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

class MobileScreen:
    def __init__(self):
        self.power_on = False
        self.screen_on = False

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

if not self.power_on or not self.screen_on:
    return

Такой стиль программирования называется «флаговым программированием». Этот термин используется для описания кода, который трудно понять из-за логики, завязанной на комбинации флагов.

Наличие флагов почти всегда ведет к такой сложности, так как количество состояний у системы обычно больше двух. То есть одного флага никогда не будет достаточно.

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

  • Power Off — питание отключено, значит, и экран выключен
  • Screen Disabled — экран выключен, но питание включено
  • Screen On — экран включен

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

class MobileScreen:
    def __init__(self):
        self.state_name = "power_off"

    def power_on(self):
        self.state_name = "screen_disabled"

    def touch(self):
        if self.state_name == "power_off":
            return

        if self.state_name == "screen_disabled":
            self.state_name = "screen_on"

        self.notify("touch")

    def swipe(self):
        if self.state_name != "screen_on":
            return

        # На событие должно реагировать текущее активное приложение
        self.notify("swipe")

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

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

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

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

class MobileScreen:
    def __init__(self):
        # Список состояний нужен для переключений между ними
        # Иначе возможно появление циклических зависимостей внутри состояний
        self.states = {
            "power_off": PowerOffState,
            "screen_disabled": ScreenOffState,
            "screen_on": ScreenOnState,
        }
        # Начальное состояние
        # Внутрь передается текущий объект
        # Это нужно для смены состояний (примеры ниже)
        self.state = self.states["power_off"](self)

    def power_on(self):
        # Предыдущее состояние нас не волнует
        # Все данные хранятся в самом экране
        # Объекты-состояния не имеют своих данных
        self.state = self.states["screen_disabled"](self)

    def touch(self):
        self.state.touch()

    def swipe(self):
        self.state.swipe()

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

class PowerOffState:
    def __init__(self, screen):
        self.screen = screen

    def touch(self):
        # ничего не происходит
        pass

    def swipe(self):
        # ничего не происходит
        pass

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

class ScreenOffState:
    def __init__(self, screen):
        self.screen = screen

    def touch(self):
        # Включаем экран.
        self.screen.state = self.screen.states["screen_on"](self.screen)
        # Оповещаем текущую программу об активации
        self.screen.notify("touch")

    def swipe(self):
        # ничего не происходит
        pass

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

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

class ScreenOnState:
    def __init__(self, screen):
        self.screen = screen

    def touch(self):
        self.screen.notify("touch")

    def swipe(self):
        self.screen.notify("swipe")

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

Выводы

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


Дополнительные материалы

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

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

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff