Паттерн «Состояние» в Python
Основная идея паттерна «Состояние» заключается в инкапсуляции состояния объекта в независимые классы и делегировании поведения объекта текущему объекту состояния. Когда состояние объекта меняется, он переключается на другой объект состояния, тем самым изменяя своё поведение.
Зачем нужен паттерн «Состояние»?
Без использования паттерна «Состояние» мы обычно используем большое количествоif-elseилиswitch-caseоператоров для обработки поведения в разных состояниях. У этого подхода есть несколько проблем:
- Раздувание кода: По мере увеличения количества состояний условные операторы становятся всё сложнее
- Сложно поддерживать: Изменение поведения одного состояния может повлиять на другие состояния
- Нарушает принцип открытости/закрытости: Добавление нового состояния требует изменения существующего кода
Паттерн «Состояние» решает эти проблемы путём инкапсуляции каждого состояния в независимый класс.
Структура паттерна «Состояние»
Основные роли
Паттерн «Состояние» состоит из трёх основных ролей:
- Context (контекст): Хранит экземпляр конкретного объекта состояния; этот экземпляр определяет текущее состояние
- State (интерфейс состояния): Определяет интерфейс для инкапсуляции поведения, связанного с конкретным состоянием Context
- ConcreteState (конкретное состояние): Реализует интерфейс State; каждый класс конкретного состояния реализует поведение, связанное с состоянием Context
UML-диаграмма классов

Реализация паттерна «Состояние»
Основные шаги реализации
Давайте рассмотрим реализацию паттерна «Состояние» на простом примере. Предположим, у нас есть система лифта, в которой лифт может находиться в разных состояниях: открыт, закрыт, движется и остановлен.
Шаг 1: Определите интерфейс состояния
Пример
class ElevatorState(ABC):
"""Интерфейс состояния лифта"""
@abstractmethod
def open_doors(self):
"""Операция открытия дверей"""
pass
@abstractmethod
def close_doors(self):
"""Операция закрытия двери"""
pass
@abstractmethod
def move(self):
"""Операция движения"""
pass
@abstractmethod
def stop(self):
"""Операция остановки"""
pass
Шаг 2: Реализуйте конкретные классы состояний
Пример
"""Открытое состояние"""
def open_doors(self):
print("Дверь уже открыта")
return self
def close_doors(self):
print("Закрываем дверь...")
return DoorClosedState()
def move(self):
print("Ошибка: дверь всё ещё открыта, невозможно двигаться")
return self
def stop(self):
print("Лифт остановился")
return self
class DoorClosedState(ElevatorState):
"""Закрытое состояние"""
def open_doors(self):
print("Открываем дверь...")
return DoorOpenState()
def close_doors(self):
print("Дверь уже закрыта")
return self
def move(self):
print("Лифт начинает движение...")
return MovingState()
def stop(self):
print("Лифт остановился")
return self
class MovingState(ElevatorState):
"""Состояние движения"""
def open_doors(self):
print("Ошибка: лифт движется, невозможно открыть дверь")
return self
def close_doors(self):
print("Дверь уже закрыта")
return self
def move(self):
print("Лифт движется")
return self
def stop(self):
print("Лифт останавливается...")
return DoorClosedState()
Шаг 3: Создайте класс контекста
Пример
"""Класс контекста лифта"""
def __init__(self):
# Начальное состояние — состояние закрытой двери
self._state = DoorClosedState()
@property
def state(self):
"""Получить текущее состояние"""
return self._state
def set_state(self, state):
"""Установить новое состояние"""
self._state = state
print(f"Состояние переключено на: {state.__class__.__name__}")
def open_doors(self):
"""Операция открытия дверей"""
print("Выполняется операция открытия двери...")
self.set_state(self._state.open_doors())
def close_doors(self):
"""Операция закрытия двери"""
print("Выполняется операция закрытия двери...")
self.set_state(self._state.close_doors())
def move(self):
"""Операция движения"""
print("Выполняется операция перемещения...")
self.set_state(self._state.move())
def stop(self):
"""Операция остановки"""
print("Выполняется операция остановки...")
self.set_state(self._state.stop())
Шаг 4: Использование паттерна «Состояние»
Пример
elevator = Elevator()
# Тест переходов состояний
print("=== Тест переходов состояний лифта ===")
elevator.open_doors() # Переключение из закрытого состояния в открытое состояние
elevator.close_doors() # Переключение обратно из открытого состояния в закрытое состояние
elevator.move() # Переключение из закрытого состояния в состояние движения
elevator.stop() # Переключение обратно из состояния движения в закрытое состояние
print("\n=== Тест недопустимой операции ===)
elevator.open_doors() # Открыть дверь обычным образом
elevator.move() # Попытка движения при открытой двери (должна вызвать ошибку)
Результат выполнения
=== 电梯状态转换测试 === 执行开门操作... 正在开门... 状态已切换为: DoorOpenState 执行关门操作... 正在关门... 状态已切换为: DoorClosedState 执行移动操作... 电梯开始移动... 状态已切换为: MovingState 执行停止操作... 电梯停止中... 状态已切换为: DoorClosedState === 测试非法操作 === 执行开门操作... 正在开门... 状态已切换为: DoorOpenState 执行移动操作... 错误:门还开着,不能移动 状态已切换为: DoorOpenState
Расширенные применения паттерна «Состояние»
Паттерн «Состояние» с разделяемым состоянием
В некоторых случаях нескольким контекстам может потребоваться совместно использовать один и тот же объект состояния. Мы можем обеспечить совместное использование состояния с помощью паттерна Singleton.
Пример
"""Метакласс-одиночка"""
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
class SharedDoorOpenState(ElevatorState, metaclass=Singleton):
"""Общее открытое состояние (Singleton)"""
def open_doors(self):
print("Дверь уже открыта")
return self
def close_doors(self):
print("Закрываем дверь...")
return SharedDoorClosedState()
def move(self):
print("Ошибка: дверь всё ещё открыта, невозможно двигаться")
return self
def stop(self):
print("Лифт остановился")
return self
class SharedDoorClosedState(ElevatorState, metaclass=Singleton):
"""Общее закрытое состояние (Singleton)"""
def open_doors(self):
print("Открываем дверь...")
return SharedDoorOpenState()
def close_doors(self):
print("Дверь уже закрыта")
return self
def move(self):
print("Лифт начинает движение...")
return SharedMovingState()
def stop(self):
print("Лифт остановился")
return self
class SharedMovingState(ElevatorState, metaclass=Singleton):
"""Общее состояние перемещения (Singleton)"""
def open_doors(self):
print("Ошибка: лифт движется, невозможно открыть дверь")
return self
def close_doors(self):
print("Дверь уже закрыта")
return self
def move(self):
print("Лифт движется")
return self
def stop(self):
print("Лифт останавливается...")
return SharedDoorClosedState()
Паттерн «Состояние» и машины состояний
Паттерн «Состояние» часто используется в сочетании с машинами состояний. Ниже приведён более сложный пример машины состояний:
Пример
"""Интерфейс состояния торгового автомата"""
@abstractmethod
def insert_coin(self, machine, amount):
"""Вставить монету"""
pass
@abstractmethod
def select_product(self, machine, product):
"""Выбрать товар"""
pass
@abstractmethod
def dispense(self, machine):
"""Выдать товар"""
pass
@abstractmethod
def refund(self, machine):
"""Возврат"""
pass
class WaitingState(VendingMachineState):
"""Состояние ожидания монеты"""
def insert_coin(self, machine, amount):
print(f"Внесено {amount} юаней")
machine.balance += amount
return HasMoneyState()
def select_product(self, machine, product):
print("Пожалуйста, сначала вставьте монету")
return self
def dispense(self, machine):
print("Пожалуйста, сначала вставьте монету и выберите товар")
return self
def refund(self, machine):
print("Нет суммы для возврата")
return self
class HasMoneyState(VendingMachineState):
"""Состояние внесённой монеты"""
def insert_coin(self, machine, amount):
print(f"Продолжайте вносить {amount} юаней")
machine.balance += amount
return self
def select_product(self, machine, product):
if product.price <= machine.balance:
print(f"Выбран товар: {product.name}")
machine.selected_product = product
return ProductSelectedState()
else:
print("Недостаточно средств, пожалуйста, продолжайте вставлять монеты или выберите товар дешевле")
return self
def dispense(self, machine):
print("Сначала выберите товар")
return self
def refund(self, machine):
print(f"Возврат {machine.balance} yuan")
machine.balance = 0
return WaitingState()
class ProductSelectedState(VendingMachineState):
"""Состояние выбранного товара"""
def insert_coin(self, machine, amount):
print("Товар уже выбран, нельзя вставлять больше монет")
return self
def select_product(self, machine, product):
print("Товар уже выбран, пожалуйста, сначала завершите текущую транзакцию")
return self
def dispense(self, machine):
product = machine.selected_product
change = machine.balance - product.price
print(f"Выдача: {product.name}")
if change > 0:
print(f"Сдача: {change} yuan")
machine.balance = 0
machine.selected_product = None
return WaitingState()
def refund(self, machine):
print(f"Возврат {machine.balance} yuan")
machine.balance = 0
machine.selected_product = None
return WaitingState()
class Product:
"""Класс Product"""
def __init__(self, name, price):
self.name = name
self.price = price
class VendingMachine:
"""Торговый автомат"""
def __init__(self):
self._state = WaitingState()
self.balance = 0
self.selected_product = None
def set_state(self, state):
self._state = state
def insert_coin(self, amount):
print(f"Операция: вставить монету {amount} yuan")
self.set_state(self._state.insert_coin(self, amount))
def select_product(self, product):
print(f"Операция: выбрать товар {product.name}")
self.set_state(self._state.select_product(self, product))
def dispense(self):
print("Операция: запросить выдачу товара")
self.set_state(self._state.dispense(self))
def refund(self):
print("Операция: Запрос возврата")
self.set_state(self._state.refund(self))
Плюсы и минусы паттерна «Состояние»
Преимущества
- Принцип единственной ответственности: Поместите код, относящийся к конкретному состоянию, в независимые классы
- Принцип открытости/закрытости: Вводите новые состояния, не изменяя существующие классы состояний и контекст
- Устранение условных операторов: Устраните громоздкие условные ветвления с помощью полиморфных вызовов
- Четкие переходы между состояниями: Сделайте переходы между состояниями более явными, уменьшая количество ошибок, вызванных переходами между состояниями
Недостатки
- Возможное избыточное проектирование: Если количество состояний невелико или редко меняется, использование паттерна «Состояние» может оказаться чрезмерно сложным
- Увеличение количества классов состояний: Каждое состояние требует соответствующего класса, что может привести к увеличению количества классов
- Связанность контекста и состояния: Контексту необходимо знать все конкретные классы состояний, чтобы выполнять переходы между состояниями
Практические упражнения
Упражнение 1: Улучшите систему лифта
Попробуйте добавить следующие возможности в систему лифта:
- Добавьте состояние «Maintenance», в котором запрещены все операции
- Добавьте возможность выбора этажа, при которой этажи можно выбирать только в остановленном состоянии
- Реализуйте автоматические переходы состояний при прибытии на этаж
Упражнение 2: Реализуйте систему светофора
Используйте паттерн «Состояние» для реализации системы светофора с тремя состояниями: красный свет, зелёный свет и жёлтый свет. Правила переходов состояний следующие:
- Красный свет → Зелёный свет (через 30 секунд)
- Зелёный свет → Жёлтый свет (через 25 секунд)
- Жёлтый свет → Красный свет (через 5 секунд)
Упражнение 3: Состояния игрового персонажа
Разработайте систему состояний для игрового персонажа, включающую следующие состояния:
- Обычное состояние: может двигаться и атаковать
- Состояние отравления: непрерывно теряет здоровье, скорость передвижения уменьшена вдвое
- Состояние оглушения: не может двигаться или атаковать
- Состояние неуязвимости: иммунитет к урону
Итог
Паттерн «Состояние» — это мощный паттерн проектирования, особенно подходящий для сценариев, где поведение объекта зависит от его состояния и где существует множество состояний со сложными переходами. Инкапсулируя каждое состояние как независимый класс, паттерн «Состояние» делает код более понятным, простым в сопровождении и расширении.
Ключевые моменты:
- Паттерн «Состояние» устраняет условные операторы, инкапсулируя состояния как объекты
- Контекст делегирует поведение, связанное с состоянием, текущему объекту состояния
- Переходы между состояниями могут запускаться контекстом или самими объектами состояний
- Разумное использование паттерна «Состояние» может значительно повысить сопровождаемость кода