Намір

Інкапсулювання запиту у якості об’єкта, що дає змогу параметризувати клієнти за допомогою різних запитів, створюючи чергу запитів і підтримування зворотніх операцій.

Також відомий як

Акція (Action), Транзакція (Transaction)

Мотивація

Інколи необхідно надіслати запити до об’єктів без інформації про операції, що будуть надсилатися, або про отримувача запитів. Наприклад, інструментарій інтерфейсу користувача включає об’єкти на подобі кнопок і меню, які надсилають запити, реагуючи на ввід користувача. Але інструментарій не може реалізувати запити безпосередньо у кнопці або меню, через те що тільки програми які використовують інструментарій знають, що необхідно виконати на якому об’єкті. У якості дизайнерів інструментарію ми не маємо шляху отримання інформації про отримувача запиту або операції які він виконуватиме. Шаблон проектування Команда (Command) дозволяє об’єктам інструментарію надсилати запити до невизначених об’єктів програми за допомогою перетворення самого запиту на об’єкт. Даний об’єкт може зберігатися і передаватися на подобі інших об’єктів. Ключом до цього шаблону являється абстрактний клас команди, який оголошує інтерфейс для виконування операцій. У найпростішій формі даний інтерфейс включає абстрактну операцію Виконати (Execute). Конкретні потомки класу Command визначають пару отримувач-дія, зберігаючи отримувача у якості примірника і реалізовуючи метод Execute для виконання запиту. Отримувач має необхідну інформацію для виконання запиту. Меню можуть легко реалізовуватись за допомогою об’єктів класу команди Command. Кожен вибір у Menu являється примірником класу MenuItem. Клас Application створює дані меню і їхні пункти разом з іншими елементами інтерфейсу користувача. Клас Application також утримує об’єкти Document, які відкрив користувач. Програма конфігурує кожний примірник MenuItem за допомогою примірника конкретного потомка класу Command. Коли користувач обирає пункт MenuItem, він в свою чергу викликає Execute на його примірнику команди, і метод Execute виконує операцію. Примірники MenuItem не знають який потомок класу Command вони використовують. Потомки класу Command зберігають отримувача запиту і викликають одну або більше операцій на отримувачу. Наприклад, команда PasteCommand підтримує вставку тексту з буферу обміну в документ Document. Отримувач команди PasteCommand являється об’єкт Document, який він отримує у якості примірника. Операція Execute викликає метод Paste на докуметі-отримувачу Document. Операція Execute класу OpenCommand є іншою: вона опитує користувача про ім’я документу, створює відповідний об’єкт класу Document, додає документ до програми і відкриває його. Деколи команда меню MenuItem повинна виконати послідовність команд. Наприклад, об’єкт MenuItem для центрування сторінки з нормальним розміром може бути сконструйований з об’єкту CenterDocumentCommand i aNormalSizeCommand. Через те, що це загальноприйнято об’єднювати команди подібним шляхом, ми можемо визначити клас MacroCommand, щоб дозволити об’єкту aMenuItem виконувати довільне число команд. MacroCommand являється конкретним потомком класу Command, який просто виконує послідовність об’єктів Command. MacroCommand немає явного отримувача, тому що команди, які він утримує визначають їхній власний утримувач. Зверніть увагу як у кожному з цих прикладів шаблон Команда відокремлює об’єкт, який викликає операцію від того, як її виконувати. Це дає нам велику гнучкість у створенні наших інтерфейсів. Програма може забезпечити і пункти меню і кнопки інструментів, просто створюючи меню і кнопку з одним примірником однакового конкретного примірника потомка класу Command. Ми можемо замінити команду під час виконання команди, що може бути корисним для реалізації контекстно залежних меню. Ми також можемо підтримувати скриптування команд, складаючи команди у групи. Усе це можливо через те, що об’єкт який викликає запит повинен знати тільки як його викликати; він не повинен знати як запит буде виконуватися.

Застосовуваність

Використовуйте шаблон Команда коли ви бажаєте:
  • параметризувати об’єкти за допомогою дій, які вони можуть виконати, на подобі об’єктів меню MenuItem, як показано вище. Ви можете виразити таку параметризацію у процедурних мовах за допомогою функції (callback), тобто, функція яка зареєстрована в певному місці, для того щоб бути виконаною пізніше. Команди являються об’єктно-орієнтованою заміною для таких функцій.
  • Вказувати, чергувати, і виконувати запити при різних точках часу. Об’єкт класу Command може мати життєвий цикл незалежний від оригінального запиту. Якщо отримувач запиту може бути представлений у спосіб незалежний від адресного простору, тоді ви можете передавати об’єкти команди для запиту до іншого процесу і заповнювати запит у ньому.
  • Підтримка відміни операцій. Операція Execute класу Command може зберігати стан для відміни її дій на самій команді. Інтерфейс класу Command повинен мати додаткову операцію Unexecute, яка повертає ефекти попередньої дії методу Execute. Виконані команди зберагіються у списку історії. Необмежена кількість відновлень і повторень досягається за допомогою проходу цього списку в зворотньому або в прямому порядку, викликаючи Unexecute i Execute відповідно. Підтримка журналювання змін, отож вони можуть повторно застосованими у випадку краху системи. Додаванням до інтерфейсу класу Command операцій завантаження і збереження, ви можете утримувати постійний журнал змін. Відновлення з краху спричиняє завантаження команд з журналу з диска і повторне виконання їх за допомогою операції Execute.
  • Структурування системи навколо високорівневих операцій, які збудовані на примітивних. Така структура являється загально прийнятою для інформаційних систем, які підтримують транзакції. Транзакція інкапсулює набір змін у дані. Шаблон Команда пропонує шлях для моделювання транзакцій. Команди мають загальний інтерфейс, що дозволяє вам викликати усі транзакції однаковим способом. Шаблон також полегшує розширення системи новими транзакціями.

Структура

Учасники

  • Команда (Command)
    • оголошує інтерфейс для виконання операції.
  • Конкретна команда (ConcreteCommand: PasteCommand, OpenCommand)
    • визначає зв’язок між об’єктом Receiver і операцією.
    • Реалізовує команду Execute за допомогою виклику відповідних операцій на отримувачу Receiver.
  • Клієнт (Client — Application)
    • створює об’єкт ConcreteCommand і встановлює його отримувач.
  • Викликаючий об’єкт (Invoker: MenuItem)
    • опитує команду для виклику запиту
  • Отримувач (Receiver: Document, Application)
    • знає як викликати операції асоційовані з даним запитом. Будь-який клас може виступати у якості отримувача Receiver.

Співпрацювання

  • Клієнт створює об’єкт ConcreteCommand і вказує його отримувача.
  • Об’єкт-викликач зберігає об’єкт ConcreteCommand.
  • Об’єкт-викликач запитує виконання операції за допомогою методу Execute на команді. Коли команди являються зворотніми, об’єкт ConcreteCommand зберігає стан для відміни команди перед викликом Execute.
  • Об’єкт ConcreteCommand викликає операції на його отримувачі для виконання запиту.
Наступна діаграма показує взаємодії між цими об’єктами. Вона ілюструє як клас Command розбиває викликач від отримувача (і запит який він обслуговує).

Наслідки

Шаблон команда має наступні наслідки:
  1. Команда розділює об’єкт, який викликає операцію і саму операцію, звільняючи його від інформації про її реалізацію.
  2. Команди являються першокласними об’єктами. Вони можуть використовуватись і розширюватись, так як будь-який інший об’єкт.
  3. Ви можете зібрати команди у складену команду. Прикладом являється клас MacroCommand, описаний раніше. В загальному, складені команди являються примірниками шаблону Композитор.
  4. Легко додавати нові команди, через те що вам не потрібно змінювати існуючі класи.

Реалізація

Розглянемо наступні проблеми, які можуть виникнути під час реалізації шаблону Команда:
  1. На скільки розумною повинна бути команда? Команда може мати широкий спектр можливостей. В одній крайності вона буквально визначає зв’язок між отримувачем і операціями (actions), що виконують запит. В іншій крайності вона реалізовує усе самостійно взагалі без делегування до отримувача. Остання крайність корисна коли ви бажаєте визначити команди, що є незалежними від існуючих класів, коли не існує жоден придатний отримувач, або коли команда знає свого отримувача неявно. Наприклад, команда яка створює вікно програми, може бути здатною створювати його як будь-який інший об’єкт. Десь посередині цих крайностей розміщується команда, яка має достатньо знань для знаходження їхнього отримувача динамічно.
  2. Підтримка відміни і повторення команди. Команди можуть підтримувати можливості відміни і повторення операцій, якщо вони забезпечують шлях для обернення їхнього виконання (тобто, операції Unexecute або Undo). Клас AconcreteCommand може потребувати зберігати додаткові стани для цього. Даний стан може включати:
    • об’єкт отримувач, який безпосередньо виконує операції у відповідь на запити,
    • аргументи до операції виконані на отримувачу, і
    • будь-які оригінальні значення у отримувачі, які можуть змінюватися у результаті виконання запиту. Отримувач повинен забезпечувати операції, які дозволяють команду повертати отримувач до його попереднього стану.
    Для підтримки одного рівня відміни, програма потребує зберігати тільки команду, яка була виконаною останньою. Для багатьох рівнів відміни, програма повинна зберігати список історії команд, які були виконаними, де максимальна довжина списку визначає кількість рівнів відміни/повторення. Список історії зберігає послідовність команд, яка була виконаною. Переміщаючись в зворотньому напрямку списку з відміною дії команд, видаляє їх ефект; переміщаючись в прямому порядку і виконуючи команди повторно повертає їхній ефект. Відворотні команди можуть потребувати копіювання перед тим, як вона поміщається у списку історії. Це тому, що об’єкт команди, який виконав оригінальний запит, наприклад, з об’єкта меню MenuItem, буде виконувати інші запити пізніше. Копіювання необхідне для розділення різних викликів однієї команди, якщо її стан може змінюватися між різними викликами. Наприклад, команда DeleteCommand, яка видаляє обрані об’єкти повинна зберігати різні множини об’єктів під час її виконання. Тому об’єкт DeleteCommand повинен бути скопійованим після виконання, і копія повинна бути розміщеною у списку. Якщо стан команди ніколи не змінюється під час виконання, тоді копіювання не вимагається — тільки посилання на команду повинно бути розміщене у списку історії. Команди, які повинні бути скопійованими перед тим як розміщуватися у списку історії поводять себе як прототипи (перегляньте шаблон Прототип).
  3. Запобігання накопичення помилок у процесі відміни команд. Гістерезим може бутипроблемою у забезпечення надійного механізму відміни/повторення. Помилки можуть акумулюватися під час виконання команд, відміни і повторного виконання, і як наслідок кінцевий стан програми відрізняється від оригінального. Може виникнути необхідність зберегти більше інформації у команді щоб впевнитись, що об’єкти будуть відновлювати їхній початковий оригінальний стан. Шаблон Пам’ятка (Memento) може застосовуватися для надання командам доступ до даної інформаці без розкриття нутрощів об’єкту.
  4. Використання шаблонів C++. Для команд, які (1) не є зворотніми і (2) не вимагають аргументів, ми можемо використовувати шаблони C++ для запобігання створення потомків класу Command для кожного типу дії і отримувача. Ми покажемо як це зробити у секції прикладу коду.

Приклад коду

Код C++ просто показує реалізацію класів Command у секції Мотивації. Ми визначимо OpenCommand, PasteCommand, і MacroCommand. Спочатку, абстрактний клас команди Command:
class Command {
 public:
     virtual ~Command () ;
     virtual void Execute () = 0 ;

 protected:
     Command () ;
} ;
Команда OpenCommand відкриває документ ім’я якого надане користувачем. Команда OpenCommand повинна передаватися об’єкту anApplication у його конструктору. Клас AskUser являється реалізацію підпрограми, яка опитує користувача про ім’я документу для відкриття.
class OpenCommand : public Command {
public:
	OpenCommand(Application*) ;
	virtual void Execute() ;

protected:
	virtual const char* AskUser() ;

private:
	Application* _application ;
	char* _response ;
};

OpenCommand::OpenCommand (Application* a) {
	_application = a;
}

void OpenCommand::Execute () {
	const char* name = AskUser();
	if (name != 0) {
		Document* document = new Document(name);
		_application->Add(document);
		document->Open();
	}
}
Команда PasteCommand повинна бути переданою до об’єкту Document і містить отримувача. Отримувач передається у якості параметра до конструктора класу PasteCommand.
class PasteCommand : public Command {
public:
    PasteCommand (Document*) ;
    virtual void Execute() ;

private:
    Document* _document ;
} ;
PasteCommand::PasteCommand (Document* doc) { _document = doc ; }
void PasteCommand::Execute () {
    _document->Paste () ;
}
Для простих команд, які не є відворотніми і не вимагають аргументів, ми можемо використовувати шаблонний клас для параметризації отримувача команди. Ми визначемо потомок класу SimpleCommand для таких команд. Об’єкти SimpleCommand конфігурується типом Receiver і відповідає за зв’язування між об’єктом отримувача і дією збереженою у якості вказівника на метод.
template <class Receiver>
class SimpleCommand : public Command {
 public :
    typedef void (Receiver::* Action) () ;
    SimpleCommand(Receiver* r, Action a) :
    _receiver(r), _action(a) { }
    virtual void Execute() ;

 private:
    Action _action ;
    Receiver* _receiver ;
};
Конструктор зберігає отримувача і дію у відповідних примірниках змінних. Метод Execute просто застосовує дію до отримувача.
template <class Receiver>
void SimpleCommand&ltReceiver>::Execute () {
(_receiver->*_action)();
}
Для створення команди, яка викликає дію Action примірник класу MyClass, клієнт просто виконує:
MyClass* receiver = new MyClass;
// ...
Command* aCommand =
new SimpleCommand<MyClass>(receiver, &MyClass::Action);
// ...
aCommand->Execute();
Майте на увазі, що це рішення працює тільки для простих команд. Більш складні команди, які реєструють не тільки їхніх отримувачі, але також аргументи і/або стан відміни команди вимагають потомків класу Command. Клас MacroCommand управляє послідовністю підкоманд і надає операції для додавання і видаляння підкоманд. Не потрібно безпосередніх отримувачів, тому що підкоманди вже визначають їхніх отримувачів. Ключовим елементом класу MacroCommand являється її метод Execute. Вона преберає усі підкоманди і виконує метод Execute на кожному з них.
void MacroCommand::Execute () {
    ListIterator<Command*> i(_cmds) ;
    for (i.First(); !i.IsDone(); i.Next()) 
    {
        Command* c = i.CurrentItem() ;
        c->Execute() ;
    }
}
Зверніть увагу, якщо MacroCommand реалізовує операцію відміни anUnexecute, тоді її підкоманди повинні підтримувати відміну їхньої дії у зворотньому напрямку по відношенню до реалізації Execute. Накінець, MacroCommand повинна забезпечувати операції для управління її підкомандами. Клас MacroCommand також відповідальний за видалення її підкоманд.
void MacroCommand::Add (Command* c) { _cmds->Append(c); }
void MacroCommand::Remove (Command* c) { _cmds->Remove(c); }

Відомі використання

Ймовірно перший приклад шаблону Команда з’являється у публікації Ліберман (Lieberman Lie85). MacApp популяризує поняття команд для реалізації відміни операцій. ET++, InterViews і Unidraw також визначають клас, які слідують шаблону Command. InterViews визначає абстрактний клас Action, який забезпечує команду функціональністю. Вона також визначає шаблон ActionCallback, який конфігурується методом дії, що може створювати підкласи команд автоматично. Бібліотека класів THINK також використовує команди для підтримки відміни дій. Команди у THINK називаються “Завданнями” (“Tasks”). Об’єкти завдань передаються разом з Ланцюгом Відповідальності. Об’єкти команд у Unidraw унікальні у тому, що вони можуть поводити себе як повідомлення. Команда Unidraw може надсилатися до іншого об’єкту для інтерпретації, і результат інтерпретації змінюється в залежності від об’єкта отримувача. Більше того, отримувач може делегувати інтерпретацію до іншого об’єкту, зазвичай до батьківського елемента отримувача у більшій структурі Ланцюжка Відповідальності. Отримувач команди у Unidraw являється скоріш обчисленим ніж збереженим. Механізм інтерпретації Unidraw залежить від інформації про тип під час виконання програми. Коплін (Coplien) описує як реалізовувати функтори, об’єкти, які являють собою функції у C++. Він досягає деяку прозорість у їхньому використанні за допомогою перевантаження методу-оператора дужок. Шаблон Команда є іншим; він фокусується на підтримці зв’язку між отримувачем і функцією (тобто дією), а не просто підтримки функції.

Пов’язані шаблони

Композитор може використовуватися для реалізації MacroCommad. Пам’ятка може утримувати стан команди, яка вимагає відміну її дії. Команда, яка повинна бути скопійована перед тим як поміщатись у списку історії діє як Прототип.