Команда (шаблон проектирования)
Команда (англ. Command) — поведенческий шаблон проектирования, используемый при объектно-ориентированном программировании, представляющий действие. Объект команды заключает в себе само действие и его параметры.
Команда | |
---|---|
Command | |
Тип | поведенческий |
Назначение | для обработки команды в виде объекта |
Родственные шаблоны | Компоновщик, Хранитель, Прототип, Одиночка |
Описан в Design Patterns | Да |
Цель
Создание структуры, в которой класс-отправитель и класс-получатель не зависят друг от друга напрямую. Организация обратного вызова к классу, который включает в себя класс-отправитель.
Описание
В объектно-ориентированном программировании шаблон проектирования Команда является поведенческим шаблоном, в котором объект используется для инкапсуляции всей информации, необходимой для выполнения действия или вызова события в более позднее время. Эта информация включает в себя имя метода, объект, который является владельцем метода и значения параметров метода.
Четыре термина всегда связаны с шаблоном Команда: команды (command), приёмник команд (receiver), вызывающий команды (invoker) и клиент (client). Объект Command знает о приёмнике и вызывает метод приемника. Значения параметров приёмника сохраняются в команде. Вызывающий объект (invoker) знает, как выполнить команду и, возможно, делает учёт и запись выполненных команд. Вызывающий объект (invoker) ничего не знает о конкретной команде, он знает только об интерфейсе. Оба объекта (вызывающий объект и несколько объектов команд) принадлежат объекту клиента (client). Клиент решает, какие команды выполнить и когда. Чтобы выполнить команду он передает объект команды вызывающему объекту (invoker).
Использование командных объектов упрощает построение общих компонентов, которые необходимо делегировать или выполнять вызовы методов в любое время без необходимости знать методы класса или параметров метода. Использование вызывающего объекта (invoker) позволяет вести учёт выполненных команд без необходимости знать клиенту об этой модели учёта (такой учёт может пригодиться, например, для реализации отмены и повтора команд).
Применение
Шаблон Команда может быть полезен в следующих случаях.
- Кнопки пользовательского интерфейса и пункты меню
В Swing и Borland Delphi Action (действие) является объектом команды. В дополнение к способности выполнить нужную команду, Action может иметь связанную с ним иконку, сочетание клавиш, текст всплывающей подсказки и так далее. Кнопка на панели инструментов или пункт меню могут быть полностью инициализированы с использованием только объекта Action.
- Запись макросов
Если все действия пользователя представлены в виде объектов команды, программа может записать последовательность действий, просто сохраняя список командных объектов в том порядке, в котором они выполняются. Затем она может «воспроизвести» одни и те же действия, выполняя те же объекты команд в той же последовательности.
- Многоуровневая отмена операций (Undo)
Если все действия пользователя в программе реализованы в виде командных объектов, программа может сохранить стек последних выполненных команд. Когда пользователь хочет отменить команду, программа просто выталкивает последний объект команды и выполняет его метод undo().
- Сети
Можно отправить объекты команд по сети для выполнения на другой машине, например действие игрока в компьютерной игре.
- Индикаторы выполнения
Предположим, что программа имеет последовательность команд, которые она выполняет по порядку. Если каждый объект команды имеет метод getEstimatedDuration() (получить оценочную длительность), программа может легко оценить общую продолжительность процесса. Она может показать индикатор выполнения, который отражает, насколько близка программа к завершению всех задач.
- Пулы потоков
Типичный класс пула потоков общего назначения может иметь метод addTask(), который добавляет рабочий элемент к внутренней очереди заданий ожидающих своего выполнения. Он поддерживает пул потоков, которые выполняют команды из очереди. Элементы в очереди являются объектами команд. Как правило, эти объекты реализуют общий интерфейс, такой как java.lang.Runnable, что позволяет пулу потоков запустить команды на выполнение, даже если он сам был написан без каких-либо знаний о конкретных задачах, для которых он будет использоваться.
- Транзакции
Аналогично операции «отмена» система управления базами данных (СУБД) или установщик программного обеспечения может хранить список операций, которые были или будут выполнены. Если одна из них закончится неудачей, то все остальные могут быть отменены или быть отброшены (обычно называется откат). Например, если две связанные между собой таблицы базы данных должны быть обновлены, а второе обновление терпит неудачу, то система может откатить транзакцию, чтобы первая таблица не содержала недопустимую ссылку.
Часто мастер (мастер установки или любой другой) представляет несколько страниц конфигурации для одного действия, которое происходит только тогда, когда пользователь нажимает на кнопку «Готово» на последней странице. В этих случаях, естественный способ отделить код пользовательского интерфейса от кода приложения является реализация мастера с помощью объекта команд. Объект команда создается при первом отображении мастера. Каждая страница мастера сохраняет свои изменения в объекте команды, поэтому объект заполняется по мере перехода пользователя. Кнопка «Готово» просто запускает метод execute() на выполнение.
Примеры
Пример на C++
#include <iostream>
#include <vector>
#include <string>
using namespace std;
class Document
{
vector<string> data;
public:
Document()
{
data.reserve(100); // at least for 100 lines
}
void Insert( int line, const string & str )
{
if ( line <= data.size() )
data.insert( data.begin() + line, str );
else
cout << "Error!" << endl;
}
void Remove( int line )
{
if( !( line>data.size() ) )
data.erase( data.begin() + line );
else
cout << "Error!" << endl;
}
string & operator [] ( int x )
{
return data[x];
}
void Show()
{
for( int i = 0; i<data.size(); ++i )
{
cout << i + 1 << ". " << data[i] << endl;
}
}
};
class Command
{
protected:
Document * doc;
public:
virtual ~Command() {}
virtual void Execute() = 0;
virtual void unExecute() = 0;
void setDocument( Document * _doc )
{
doc = _doc;
}
};
class InsertCommand : public Command
{
int line;
string str;
public:
InsertCommand( int _line, const string & _str ): line( _line ), str( _str ) {}
void Execute()
{
doc->Insert( line, str );
}
void unExecute()
{
doc->Remove( line );
}
};
class Invoker
{
vector<Command*> DoneCommands;
Document doc;
Command* command;
public:
void Insert( int line, string str )
{
command = new InsertCommand( line, str );
command->setDocument( &doc );
command->Execute();
DoneCommands.push_back( command );
}
void Undo()
{
if( DoneCommands.size() == 0 )
{
cout << "There is nothing to undo!" << endl;
}
else
{
command = DoneCommands.back();
DoneCommands.pop_back();
command->unExecute();
// Don't forget to delete command!!!
delete command;
}
}
void Show()
{
doc.Show();
}
};
int main()
{
char s = '1';
int line, line_b;
string str;
Invoker inv;
while( s!= 'e' )
{
cout << "What to do: \n1.Add a line\n2.Undo last command" << endl;
cin >> s;
switch( s )
{
case '1':
cout << "What line to insert: ";
cin >> line;
--line;
cout << "What to insert: ";
cin >> str;
inv.Insert( line, str );
break;
case '2':
inv.Undo();
break;
}
cout << "$$DOCUMENT$$" << endl;
inv.Show();
cout << "$$DOCUMENT$$" << endl;
}
}
Пример на Python
from abc import ABCMeta, abstractmethod
class Troop:
"""
Receiver - объект военного отряда
"""
def move(self, direction: str) -> None:
"""
Начать движение в определенном направлении
"""
print('Отряд начал движение {}'.format(direction))
def stop(self) -> None:
"""
Остановиться
"""
print('Отряд остановился')
class Command(metaclass=ABCMeta):
"""
Базовый класс для всех команд
"""
@abstractmethod
def execute(self) -> None:
"""
Приступить к выполнению команды
"""
pass
@abstractmethod
def unexecute(self) -> None:
"""
Отменить выполнение команды
"""
pass
class AttackCommand(Command):
"""
Команда для выполнения атаки
"""
def __init__(self, troop: Troop) -> None:
"""
Constructor.
:param troop: отряд, с которым ассоциируется команда
"""
self.troop = troop
def execute(self) -> None:
self.troop.move('вперед')
def unexecute(self) -> None:
self.troop.stop()
class RetreatCommand(Command):
"""
Команда для выполнения отступления
"""
def __init__(self, troop: Troop) -> None:
"""
Constructor.
:param troop: отряд, с которым ассоциируется команда
"""
self.troop = troop
def execute(self) -> None:
self.troop.move('назад')
def unexecute(self) -> None:
self.troop.stop()
class TroopInterface:
"""
Invoker - интерфейс, через который можно отдать команды определенному отряду
"""
def __init__(self, attack: AttackCommand, retreat: RetreatCommand) -> None:
"""
Constructor.
:param attack: команда для выполнения атаки
:param retreat: команда для выполнения отступления
"""
self.attack_command = attack
self.retreat_command = retreat
self.current_command = None # команда, выполняющаяся в данный момент
def attack(self) -> None:
self.current_command = self.attack_command
self.attack_command.execute()
def retreat(self) -> None:
self.current_command = self.retreat_command
self.retreat_command.execute()
def stop(self) -> None:
if self.current_command:
self.current_command.unexecute()
self.current_command = None
else:
print('Отряд не может остановиться, так как не двигается')
if __name__ == '__main__':
troop = Troop()
interface = TroopInterface(AttackCommand(troop), RetreatCommand(troop))
interface.attack()
interface.stop()
interface.retreat()
interface.stop()
Пример на PHP5
<?php
/**
* Абстрактый класс "команды"
* @abstract
*/
abstract class Command
{
public abstract function Execute();
public abstract function UnExecute();
}
/**
* Класс конкретной "команды"
*/
class CalculatorCommand extends Command
{
/**
* Текущая операция команды
*
* @var string
*/
public $operator;
/**
* Текущий операнд
*
* @var mixed
*/
public $operand;
/**
* Класс, для которого предназначена команда
*
* @var object of class Calculator
*/
public $calculator;
/**
* Конструктор
*
* @param object $calculator
* @param string $operator
* @param mixed $operand
*/
public function __construct($calculator, $operator, $operand)
{
$this->calculator = $calculator;
$this->operator = $operator;
$this->operand = $operand;
}
/**
* Переопределенная функция parent::Execute()
*/
public function Execute()
{
$this->calculator->Operation($this->operator, $this->operand);
}
/**
* Переопределенная функция parent::UnExecute()
*/
public function UnExecute()
{
$this->calculator->Operation($this->Undo($this->operator), $this->operand);
}
/**
* Какое действие нужно отменить?
*
* @private
* @param string $operator
* @return string
*/
private function Undo($operator)
{
//каждому произведенному действию найти обратное
switch($operator)
{
case '+': $undo = '-'; break;
case '-': $undo = '+'; break;
case '*': $undo = '/'; break;
case '/': $undo = '*'; break;
default : $undo = ' '; break;
}
return $undo;
}
}
/**
* Класс получатель и исполнитель "команд"
*/
class Calculator
{
/**
* Текущий результат выполнения команд
*
* @private
* @var int
*/
private $curr = 0;
public function Operation($operator,$operand)
{
//выбрать оператора для вычисления результата
switch($operator)
{
case '+': $this->curr+=$operand; break;
case '-': $this->curr-=$operand; break;
case '*': $this->curr*=$operand; break;
case '/': $this->curr/=$operand; break;
}
print("Текущий результат = $this->curr (после выполнения $operator c $operand)");
}
}
/**
* Класс, вызывающий команды
*/
class User
{
/**
* Этот класс будет получать команды на исполнение
*
* @private
* @var object of class Calculator
*/
private $calculator;
/**
* Массив операций
*
* @private
* @var array
*/
private $commands = array();
/**
* Текущая команда в массиве операций
*
* @private
* @var int
*/
private $current = 0;
public function __construct()
{
//создать экземпляр класса, который будет исполнять команды
$this->calculator = new Calculator();
}
/**
* Функция возврата отмененных команд
*
* @param int $levels количество возвращаемых операций
*/
public function Redo($levels)
{
print("\n---- Повторить $levels операций ");
// Делаем возврат операций
for ($i = 0; $i < $levels; $i++)
if ($this->current < count($this->commands) - 1)
$this->commands[$this->current++]->Execute();
}
/**
* Функция отмены команд
*
* @param int $levels количество отменяемых операций
*/
public function Undo($levels)
{
print("\n---- Отменить $levels операций ");
// Делаем отмену операций
for ($i = 0; $i < $levels; $i++)
if ($this->current > 0)
$this->commands[--$this->current]->UnExecute();
}
/**
* Функция выполнения команд
*
* @param string $operator
* @param mixed $operand
*/
public function Compute($operator, $operand)
{
// Создаем команду операции и выполняем её
$command = new CalculatorCommand($this->calculator, $operator, $operand);
$command->Execute();
// Добавляем операцию к массиву операций и увеличиваем счетчик текущей операции
$this->commands[]=$command;
$this->current++;
}
}
$user = new User();
// Произвольные команды
$user->Compute('+', 100);
$user->Compute('-', 50);
$user->Compute('*', 10);
$user->Compute('/', 2);
// Отменяем 4 команды
$user->Undo(4);
// Вернём 3 отменённые команды.
$user->Redo(3);
Пример на Java
Для того чтобы реализовать соответствие названий операций к действию, операции над лампой (switch on, switch off) вынесены в инстанс классов SwitchOnCommand
и SwitchOffCommand
, оба класса реализуют интерфейс Command
.
import java.util.HashMap;
/** The Command interface */
interface Command {
void execute();
}
/** The Invoker class */
class Switch {
private final HashMap<String, Command> commandMap = new HashMap<>();
public void register(String commandName, Command command) {
commandMap.put(commandName, command);
}
public void execute(String commandName) {
Command command = commandMap.get(commandName);
if (command == null) {
throw new IllegalStateException("no command registered for " + commandName);
}
command.execute();
}
}
/** The Receiver class */
class Light {
public void turnOn() {
System.out.println("The light is on");
}
public void turnOff() {
System.out.println("The light is off");
}
}
/** The Command for turning on the light - ConcreteCommand #1 */
class SwitchOnCommand implements Command {
private final Light light;
public SwitchOnCommand(Light light) {
this.light = light;
}
@Override // Command
public void execute() {
light.turnOn();
}
}
/** The Command for turning off the light - ConcreteCommand #2 */
class SwitchOffCommand implements Command {
private final Light light;
public SwitchOffCommand(Light light) {
this.light = light;
}
@Override // Command
public void execute() {
light.turnOff();
}
}
public class CommandDemo {
public static void main(final String[] arguments) {
Light lamp = new Light();
Command switchOn = new SwitchOnCommand(lamp);
Command switchOff = new SwitchOffCommand(lamp);
Switch mySwitch = new Switch();
mySwitch.register("on", switchOn);
mySwitch.register("off", switchOff);
mySwitch.execute("on");
mySwitch.execute("off");
}
}
С использованием функционального интерфейса
Начиная с Java 8, не требуется обязательно создавать классы SwitchOnCommand
и SwitchOffCommand
вместо этого мы можем использовать оператор ::
как показано в следующем примере
public class CommandDemo {
public static void main(final String[] arguments) {
Light lamp = new Light();
Command switchOn = lamp::turnOn;
Command switchOff = lamp::turnOff;
Switch mySwitch = new Switch();
mySwitch.register("on", switchOn);
mySwitch.register("off", switchOff);
mySwitch.execute("on");
mySwitch.execute("off");
}
}
Пример на Swift 5
protocol Command {
func execute()
}
// Invoker
class Switch {
enum SwitchAction {
case on, off
}
var status: String?
var action: Light?
func register(_ command: Light) {
self.action = command
}
func execute(_ commandName: SwitchAction) {
if commandName == .on {
action?.turnOn()
} else if commandName == .off {
action?.turnOff()
}
}
}
// Receiver
class Light {
func turnOn() {
print("The light is ON")
}
func turnOff() {
print("The light is OFF")
}
}
class SwitchOnCommand: Command {
private var light: Light
init(_ light: Light) {
self.light = light
}
func execute() {
light.turnOn()
}
}
class SwitchOffCommand: Command {
private var light: Light
init(_ light: Light) {
self.light = light
}
func execute() {
light.turnOff()
}
}
// USE
let invoker = Switch()
let reciver = Light()
invoker.register(reciver)
invoker.execute(.on)
Ссылки
- Паттерн проектирования Command (Команда) — назначение, описание, реализация на C++, достоинства и недостатки