Также известен как Command

Команда

Суть паттерна

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

Проблема

Если вам нужно выполнить какую-то операцию, вы берёте определённый объект и вызываете нужный метод. Но что, если вам нужно выполнить операцию не сейчас, а, скажем, через 10 минут?

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

Решение

Вот тут и приходит на помощь паттерн Команда. Он предлагает превратить операции в объекты. Параметры операции станут полями этого объекта.

Например, мы вынесем операцию «вырезать» из класса Документ в класс КомандаВырезать. Чтобы объект команды понимал что же ему вырезать, мы будем подавать ему в конструктор ссылку на текущий документ.

Теперь, благодаря тому, что операции являются объектами, вы можете делать с ними всякие интересные вещи. Например, в три этапа вы могли бы реализовать систему отмены для вашей программы:

  1. Добавить в классы команд метод отменить(), который бы делал то же, что и основной метод команды, но наоборот.
  2. Сохранять все выполненные объекты команд в один список.
  3. При нажатии кнопки отмены, брать последнюю команду из списка и выполнять её метод отменить().

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

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

Заказ в ресторане

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

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

В этом примере, вы являетесь Отправителем, официант с блокнотом — Командой, а шеф — Получателем. Как и в паттерне, вы не соприкасаетесь напрямую с шефом. Вместо этого, вы отправляете заказ с официантом, который самостоятельно «настраивает» шефа на работу.

Структура

Схема структуры классов паттерна Команда
  1. Команда — это интерфейс, в котором описываются общие для всех Конкретных команд операции.

    Необходимый минимум — это операция выполнить(), которую каждая Конкретная команда будет реализовывать по-своему.

  2. Конкретная команда реализует методы интерфейса Команды, в том числе главный метод выполнить().

    Некоторые команды могут работать сами по себе, но большинство команд содержит ссылку на какой-то внешний объект бизнес-логики, называемый Получатель. Команда может вызывать методы этого объекта и передавать ему какие-то данные.

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

    После этого, передаёт Отправителю созданную команду.

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

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

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

    Команды могут вызывать методы Получателя в своих методах.

Псевдокод

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

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

// Абстрактная команда задаёт общий интерфейс для всех команд.
abstract class Command is
    field app: Application
    field editor: Editor
    field backup: text

    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // Эта абстрактная команда содержит простейший механизм отмены. Чтобы
    // сохранять более сложное состояние редактора можно использовать
    // паттерн Снимок.
    method backup() is
        backup = editor.text
        app.history.push(this)

    method undo() is
        editor.text = backup

    // Главный метод команды остаётся абстрактным, чтобы каждая конкретная
    // команда определила его по-своему.
    abstract method execute()


// Конкретные команды.
class CopyCommand extends EditorCommand is
    // Операция копирования не записывается в историю, так как она не меняет
    // состояние редактора.
    method execute() is
        app.clipboard = editor.getSelection()

class CutCommand extends EditorCommand is
    method execute() is
        // Команды, меняющие состояние редактора, добавляют себя в историю.
        backup()
        app.clipboard = editor.getSelection()
        editor.deleteSelection()

class PasteCommand implements Command is
    method execute() is
        backup()
        editor.replaceSelection(app.clipboard)


// Глобальная история команд — это стек.
class CommandHistory is
    history: array of Command

    // Последний зашедший...
    method push(c: Command) is
        Push command to the end of history array.

    // ...выходит первым.
    method pop():Command is
        Get the most recent command from history.


// Класс редактора содержит непосредственные операции над текстом. Он отыгрывает
// роль Получателя – команды делегируют ему свои действия.
class Editor is
    field text: string
    field cursorX, cursorY, selectionWidth

    method getSelection() is
        Return selected text.

    method deleteSelection() is
        Delete selected text.

    method replaceSelection(text) is
        Insert clipboard contents at current position.


// Класс приложения настраивает объекты для совместной работы. Он выступает в
// роли Отправителя — создаёт команды, чтобы выполнить какие-то действия.
class Application is
    field clipboard: string
    field editors: array of Editors
    field activeEditor: Editor
    field history: CommandHistory

    method createUI() is
        onKeyPress("Ctrl+C", this.getCopyCommand);
        onKeyPress("Ctrl+X", this.getCutCommand);
        onKeyPress("Ctrl+V", this.getPasteCommand);
        onKeyPress("Ctrl+Z", this.undo);

    // При каждом нажатии горячей клавиши создаётся новая команда. Команды могут
    // работать с несколькими редакторами одновременно, но имеют общий
    // буфер обмена.
    method getCopyCommand() is
        return (new CopyCommand(this, activeEditor)).execute()
    method getCutCommand() is
        return (new CutCommand(this, activeEditor)).execute()
    method getPasteCommand() is
        return (new PasteCommand(this, activeEditor)).execute()

    // Берём последнюю команду из истории и заставляем её все отменить. Мы не
    // знаем конкретный тип команды, но это и не важно, так как каждая команда
    // знает как отменить своё действие.
    method undo() is
        command = history.pop()
        if (command != null)
            command.undo()

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

Вы хотите параметризовать объекты выполняемым действием.

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

Вы хотите ставить операции в очередь или выполнять их по расписанию.

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

Если нужна операция отмены.

Отмену почти всегда реализуют при помощи паттерна Команда. Для отмены вам нужно сохранять список выполненных операций. Как вариант, можно хранить историю в каком-то урезанном суррогате команды (например, в строке, по которой потом находить нужный контекст). Но в конечном итоге иметь объект для каждой команды окажется удобнее из-за всех фич ООП, который идут в комплекте.

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

  1. Определите какие классы в вашей программе должны играть роль Отправителей и Получателей. Обычно, Отправителем будет один из классов пользовательского интерфейса, а Получателем — класс бизнес-логики.

  2. Создайте общий интерфейс Команд. В нём должен быть определён метод выполнить.

  3. Создайте классы Конкретных команд и реализуйте методы их общего интерфейса.

  4. Добавьте в Конкретнуя команду поле для хранения объекта Получателя, если таковой нужен для её работы.

  5. Добавьте в классы Отправителей поля для хранения Команд. Измените Отправителей так, чтобы они делегировали команде реакцию на действие пользователя.

  6. В основном коде приложение, создавайте объекты Конкретных команд, подавая в них нужных Получателей. Затем, создавайте объекты Отправителей, подавая в них созданные Команды.

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

  • Убирает зависимость между разными слоями программы.
  • Простая отмена и повтор команд.
  • Отложенный запуск команд.
  • Комбинирование команд.
  • Соблюдает принцип открытости/закрытости.
  • Усложняет код программы за счёт дополнительных классов.

Отношения с другими паттернами

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

    • Цепочка обязанностей передаёт запрос отправителя через цепочку потенциальных получателей, ожидая, что какой-то из них обработает запрос.
    • Команда соединяет отправителя и получателя через подкласс.
    • Посредник заставляет отправителя и получателя коммуницировать только через себя.
    • Наблюдатель позволяет динамически подключать и отключать обработчиков запроса.
  • Цепочка обязанностей может использовать Команду, чтобы работать с запросом как с объектом.

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

  • Команда может делать Снимок состояния объекта перед тем как выполнить действие, чтобы потом это действие можно было отменить.

  • Команда и Стратегия схожи по духу, но отличаются масштабом и применением. Команду используют, чтобы превратить какое-то действие в объект. Вы берете операцию и заворачиваете её в объект. Аргументы операции превращаются в поля объекта. Этот объект теперь можно логировать, хранить в истории для отмены, передавать во внешние сервисы и так далее. С другой стороны, Стратегия описывает разные способы сделать определённое действие и позволяет взаимозаменять эти способы в каком-то большом объекте.

  • Если Команду нужно копировать перед вставкой в историю выполненных команд, вам может помочь Прототип.

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

Реализация в различных языках программирования

Java