Также известен как Действие, Транзакция, Command

Команда

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

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

Проблема

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

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

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

Но самое обидное ещё впереди. Ведь некоторые операции, например «копировать», можно вызывать из нескольких мест — нажав кнопку на панели управления, вызвав контекстное меню или просто нажав клавиши Ctrl+C. Когда в программе были только кнопки, код копирования содержался в подклассе КнопкаКопирования. Но теперь его придётся сдублировать ещё в два места.

Решение

Хорошие программы обычно структурированы в виде слоёв. Самый распространённый пример — слои интерфейса и бизнес-логики. Первый всего лишь рисует красивую картинку для пользователя. Но когда нужно сделать что-то важное, интерфейс "просит" слой бизнес-логики заняться этим.

В реальности это выглядит так: один из объектов интерфейса напрямую вызывает метод одного из объектов бизнес-логики, передавая в него какие-то параметры.

Паттерн Команда предлагает не отправлять такие вызовы напрямую, а «завернуть» их в отдельные объекты с единственным методом, который приводит вызов в действие.

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

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

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

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

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

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

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

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

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

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

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

Структура

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

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

  3. Конкретные команды реализуют различные запросы, следуя общему интерфейсу команд. Обычно, команда не делает всю работу самостоятельно, а лишь передаёт вызов получателю — определённому объекту бизнес-логики.

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

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

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

Псевдокод

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

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

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

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

    // Обычный конструктор.
    constructor Command(app: Application, editor: Editor) is
        this.app = app
        this.editor = editor

    // Конструктор клонирования.
    constructor Command(copy: Command) is
        this.app = copy.app
        this.editor = copy.editor
        this.backup = copy.backup

    // Этот метод производит копию команду, которую удобно сохранить в истории.
    // В сущности, здесь мы применяем паттерн Прототип.
    method clone() is
        return new Command(this);

    // Этот метод будет выполнен перед основным действием команды. Он сохраняет
    // состояние редактора внутри команды, а затем добавляет копию команды в
    // историю. Если потребуется отмена, состояние редактора можно будет
    // откатить к занчению, сохраненному в команде.
    method backup() is
        backup = editor.text
        app.history.push(this.clone())

    // Восстанавливаем состояние редактора.
    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 UndoCommand implements Command is
    method execute() is
        editor.undo()


// Глобальная история команд — это стек.
class CommandHistory is
    private field 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
        // ...
        copy = new CopyCommand(this, activeEditor);
        copyButton.setCommand(copy);
        shortcuts.onKeyPress("Ctrl+C", copy);

        cut = new CutCommand(this, activeEditor);
        cutButton.setCommand(cut);
        shortcuts.onKeyPress("Ctrl+X", cut);

        paste = new PasteCommand(this, activeEditor);
        pasteButton.setCommand(paste);
        shortcuts.onKeyPress("Ctrl+V", paste);

        undo = new UndoCommand(this, activeEditor);
        undoButton.setCommand(undo);
        shortcuts.onKeyPress("Ctrl+Z", undo);

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

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

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

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

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

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

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

Когда вам нужна операция отмены.

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

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

Этот способ имеет две особенности. Во-первых, точное состояние объектов не так-то просто сохранить, ведь часть его может быть приватным. Но с этим может помочь справиться паттерн Снимок.

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

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

  1. Создайте общий интерфейс Команд и определите в нём метод запуска.

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

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

    После можно заняться основным методом команды, вызывая в нем те или иные методы получателя.

  3. Добавьте в классы отправителей поля для хранения команд. Объект-отправитель должен принимать готовый объект команды извне — либо через конструктор, либо через сеттер команды.

  4. Измените основной код отправителей так, чтобы они делегировали выполнение действия команде.

  5. Порядок инициализации объектов должен выглядеть так:

    • Создайте объекты получателей.
    • Создайте объекты команд, связав их с получателями.
    • Создайте объекты отправителей, связав их с командами.

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

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

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

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

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

    • Команда устанавливает косвенную одностороннюю связь от отправителей к получателям.

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

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

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

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

  • Команду и Снимок часто используют сообща:

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

    • Для Команды важен полиморфизм, чтобы разные команды можно было подавать одному и тому же получателю.

    • С другой стороны, интерфейс Снимка слишком узок, так как рассчитан на работу с определённым исходным объектом.

  • Команда и Стратегия схожи по духу, но отличаются масштабом и применением:

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

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

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

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

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

Java