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

Команда

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

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

Проблема

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

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

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

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

Решение

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

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

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

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

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

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

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

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

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

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

Структура

Схема структуры классов паттерна Команда
  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. В основном коде приложение, создавайте объекты Конкретных команд, подавая в них нужных Получателей. Затем, создавайте объекты Отправителей, подавая в них созданные Команды.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Java