handbookНаписать

patterns/behavioral/command/README.md · тема · 4 языка · 27 тестов

Команда (Command) ​

Превращает запрос в объект: его можно сохранить, передать, выполнить позже и отменить, а тот, кто его запускает, не знает, что именно он делает.

Проблема ​

Пульт умного дома: кнопки включают свет, делают теплее, делают холоднее. Если пульт сам вызывает light.toggle() и thermostat.adjust(+1), он знает обо всех устройствах дома, а каждая новая кнопка правит его код. Хуже с отменой: чтобы откатить последнее нажатие, пульт должен помнить, какое устройство он трогал и как вернуть его назад, то есть ещё раз повторить ту же развилку if button == ..., только в обратную сторону.

Решение ​

Каждое действие становится объектом с общим интерфейсом — командой Command с двумя методами: execute() и undo(). Команда держит ссылку на получателя (Light, Thermostat), который делает настоящую работу, и параметры вызова (delta).

Вызывающий Remote знает только интерфейс. Кнопка привязывается к любой команде через bind, нажатие вызывает execute() и кладёт команду в историю, undo() снимает последнюю и откатывает её. Новое устройство — новый получатель и новая команда, пульт не меняется.

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

Структура ​

buttons, history

«interface»

Command

+execute()

+undo()

ToggleLight

-light: Light

+execute()

+undo()

AdjustTemperature

-thermostat: Thermostat

-delta: int

+execute()

+undo()

Light

+on: bool

+toggle()

Thermostat

+target: int

+adjust(delta)

Remote

-buttons: map

-history: list

+bind(button, command)

+press(button)

+undo() : bool

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

Как это собирается ​

Пульт и команды собирает приложение умного дома при запуске. Получатели (Light, Thermostat) - это устройства из списка дома, а раскладка кнопок приходит из настроек пользователя: кнопка A - свет, B - теплее на 2, C - холоднее на 1. По названию действия из настроек создаётся нужная команда с готовым получателем и параметром, и Remote получает её через bind. Сам пульт не знает, что такое свет и градусы.

Дальше нажатия идут с устройства. Нажатие на пустую кнопку D - не авария: UnknownButton ловит обработчик нажатий и показывает "not bound", история при этом не меняется. Отмена идёт по истории назад, а на пустой истории undo() честно возвращает false.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

Команды здесь обратимы сами по себе: повторный toggle() возвращает свет, adjust(-delta) отменяет adjust(delta). Они не хранят состояния, кроме получателя и параметров, поэтому одну и ту же команду можно привязать к кнопке и нажимать сколько угодно раз: в истории окажется один объект несколько раз, и каждый undo() откатит одно нажатие. Если отмене нужно помнить «что было до» (удалённый текст, прежнее значение), команду создают заново на каждое действие: это показывает практика.

ЯзыкОсобенность
PHPИнтерфейс и final readonly команды; ненайденная кнопка — ?? throw прямо в выражении
GoКоманды — структуры-значения с указателем на получателя; ошибка кнопки — сентинел ErrUnknownButton
PythonProtocol вместо интерфейса, команды — frozen dataclass, получатели — обычные dataclass
TypeScriptИнтерфейс и классы с private readonly параметрами; история — массив, pop() и ?. для пустой

Запуск: make test T=patterns/behavioral/command

Плюсы и минусы ​

Тесты одинаковы во всех языках: к кнопкам light, warmer (+1) и cooler (-2) привязаны команды, сценарий нажатий проверяется по состоянию получателей. Нажатие доходит до нужного устройства; одна кнопка дважды даёт 23 градуса; undo откатывает последнее нажатие и шаг за шагом возвращает дом к исходным 21 градусу и выключенному свету; undo на пустой истории ничего не делает. Отдельный тест — нажатие пустой кнопки: ошибка с её именем.

  • ✅ Вызывающий не зависит от получателей: новое действие — новый класс, пульт не меняется.
  • ✅ Запрос — данные: его можно хранить в истории, ставить в очередь, писать в журнал, повторять.
  • ✅ Отмена и повтор строятся поверх общего интерфейса, а не развилкой по видам действий.
  • ❌ На каждое действие — класс; для простых вызовов это лишний слой.
  • ❌ Отмена корректна, только если команда умеет вернуть ровно то состояние, что было. Обратная операция работает не всегда (ограничения, побочные эффекты), тогда команде приходится хранить снимок.
  • ❌ История держит ссылки на получателей и данные команд: на длинной сессии это память.

Когда не применять ​

  • Действие одно и отменять его не нужно: вызовите метод напрямую.
  • Язык даёт функции первого класса, а нужен только отложенный вызов без отмены: хватит замыкания или колбэка.
  • Отменяемое состояние проще целиком сохранить и восстановить (небольшой объект настроек): снимок состояния (Memento) проще пары execute/undo для каждого действия.

Отличия между языками ​

  • PHP — классический вариант GoF: интерфейс, final readonly команды, массив истории с array_pop. Без отмены команду можно было бы заменить Closure, но пара «выполнить и отменить» требует объекта.
  • Go — интерфейс с двумя методами и команды-значения, которые держат указатель на получателя: копировать их дёшево, а изменения уходят в устройство. Для команды без отмены в Go обычно хватает func(); пара функций в структуре {Do, Undo func()} — тоже рабочая идиома.
  • Python — Protocol проверяет форму без наследования; frozen=True у команд подчёркивает, что они не меняются, меняются только получатели. Для отложенного вызова без отмены идиоматичнее функция или functools.partial.
  • TypeScript — структурная типизация: любой объект с execute и undo уже Command, поэтому команду можно собрать литералом { execute: () => ..., undo: () => ... }. Классы оставлены, чтобы роли было видно в коде.

Практика ​

  • Учебные команды обратимы без памяти: повторный toggle возвращает свет. В редакторе документа так не выйдет: чтобы отменить удаление, команда должна запомнить удалённый текст, а история должна уметь не только undo, но и redo, обрезать ветку повтора и склеивать набранные подряд символы в один шаг. Практика: редактор с undo/redo показывает историю с курсором, слияние команд и правило «неудачная команда в историю не попадает».
  • Учебный получатель под рукой и отвечает сразу. Прибор учёта на другом конце радиоканала спит и теряет кадры, поэтому команду ставят в очередь и ждут от него ответа. Практика: команды прибору учёта по радио показывает очередь с одной командой в эфире, подтверждение по ожидаемому ответу, повтор по таймауту не больше N раз и отмену команды, которая ещё не ушла.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS