Паттерн Command (Команда) это поведенческий паттерн проектирования, который превращает запрос на действие в отдельный объект с единственным методом
Исходя из Википедии, можем узнать, что цель его это создание структуры, в которой класс-отправитель и класс-получатель не зависят друг от друга напрямую. Организация обратного вызова к классу, который включает в себя класс-отправитель.
В принципе написано четко и правильно, но это в теории. А как же сделать это? Вот тут начинаются проблемы, т.к. описание уже не так ясно и очевидно. Поэтому я как разобрался, решил рассказать вам как я это понял, может кому-то и пригодится:
Исходя из описания цели этого паттерна, буду комбинировать описание сразу с кодом, чтоб было нагляднее, т.к. в той же википедии обобщили для многих языков и поэтому описание отделено от примера.
В этом паттерне есть четыре термина, пока примем их как данность: команды (
execute(). Тот, кто запускает действие (invoker), знает только этот метод и не зависит от того, кто и как действие выполняет (receiver), поэтому команды можно передавать как параметры, хранить, ставить в очередь и отменять. Ниже разбор на классическом примере с лампой и выключателем: четыре роли паттерна, код каждого класса и вывод программы.Кратко
- Идея: вызов метода заворачивается в объект-команду. Вызывающий хранит команду и дергает
execute(), не зная, что внутри. - Четыре роли: команда (command), приемник команд (receiver), вызывающий команды (invoker) и клиент (client), который все связывает.
- В примере:
Lightэто receiver,TurnOnLightCommandиTurnOffLightCommandэто команды,Switchэто invoker, аTestCommandс методомmainэто client. - Команда получает приемник в конструкторе и делает ровно одно действие; новое действие добавляют новым классом-командой, не трогая
Switch. - Зачем: отделить того, кто просит действие, от того, кто его выполняет, чтобы реализацию можно было менять без ущерба для остальной системы.
- На практике так делают отмену действий, очереди задач и кнопки в интерфейсах; с Java 8 простую команду можно передать ссылкой на метод:
new Switch(l::turnOn, l::turnOff).
Привет всем, Форумчане!
Разобрался-таки основательно (как мне кажется) с паттерномCommand и хочу попытаться рассказать о нем "своими словами".
Исходя из Википедии, можем узнать, что цель его это создание структуры, в которой класс-отправитель и класс-получатель не зависят друг от друга напрямую. Организация обратного вызова к классу, который включает в себя класс-отправитель.
В принципе написано четко и правильно, но это в теории. А как же сделать это? Вот тут начинаются проблемы, т.к. описание уже не так ясно и очевидно. Поэтому я как разобрался, решил рассказать вам как я это понял, может кому-то и пригодится:
Исходя из описания цели этого паттерна, буду комбинировать описание сразу с кодом, чтоб было нагляднее, т.к. в той же википедии обобщили для многих языков и поэтому описание отделено от примера.
В этом паттерне есть четыре термина, пока примем их как данность: команды (command), приемник команд (receiver), вызывающий команды (invoker) и клиент (client).
Пример буду брать с той же Википедии, он вполне сносный.
Приемник команд (receiver): класс Light
Задача есть классLight, который умеет две вещи: включить свет и выключить. Он в терминах паттерна будет "приемник команд (receiver)"
/*Receiver class*/
public class Light{
public Light(){ }
public void turnOn(){
System.out.println("The light is on");
}
public void turnOff(){
System.out.println("The light is off");
}
}
Команда (command): интерфейс Command и его реализации
Создадим интерфейс с одним методомexecute(), который будет выполнять и который называется в терминах паттерна "команда (command)"
/*the Command interface*/
public interface Command{
void execute();
}
Необходимо инкапсулировать выполнение умений класса Light. Для этого мы создадим классы TurnOnLightCommand и TurnOffLightCommand, которые реализуют интерфейс Command и которые будут принимать в конструкторе объект класса Light. И каждый из них будет выполнять только одно действие. Один будет вызывать метод turnOn(), а другой turnOff().
/*the Command for turning on the light*/
public class TurnOnLightCommand implements Command{
private Light theLight;
public TurnOnLightCommand(Light light){
this.theLight=light;
}
public void execute(){
theLight.turnOn();
}
}
/*the Command for turning off the light*/
public class TurnOffLightCommand implements Command{
private Light theLight;
public TurnOffLightCommand(Light light){
this.theLight=light;
}
public void execute(){
theLight.turnOff();
}
}
Вызывающий команды (invoker): класс Switch
Теперь пришло время создать объект, который бы принимал эти инкапсулированные методы объектаLight. Он в терминах паттерна называется вызывающий команды (invoker). Назовем его Switch и пусть он будет принимать в конструкторе переменные Command, которые будут использоваться в созданных методах flipUp() и flipDown().
/*the Invoker class*/
public class Switch {
private Command flipUpCommand;
private Command flipDownCommand;
public Switch(Command flipUpCommand,Command flipDownCommand){
this.flipUpCommand=flipUpCommand;
this.flipDownCommand=flipDownCommand;
}
public void flipUp(){
flipUpCommand.execute();
}
public void flipDown(){
flipDownCommand.execute();
}
}
Клиент (client): класс TestCommand
Ну и конечно создадим класс, который будет использовать их, чтобы понять, что происходит вообще. Он будет иметь метод main, в котором и будет происходить все действие:
/*The test class*/
public class TestCommand{
public static void main(String[] args){
// создаем объект, который будет использоваться
Light l=new Light();
// создаем объекты для всех умений объекта Light:
Command switchUp=new TurnOnLightCommand(l);
Command switchDown=new TurnOffLightCommand(l);
// Создается invoker, с которым мы будем непосредственно контактировать:
Switch s=new Switch(switchUp,switchDown);
// вот проверка этого, используем методы:
s.flipUp();
s.flipDown();
}
}
На выводе будет следующее:
The light is on
The light is off
С Java 8 интерфейс Command с единственным абстрактным методом это функциональный интерфейс, поэтому для таких простых команд отдельные классы не обязательны: достаточно передать Switch ссылки на методы приемника.
Light l = new Light();
Switch s = new Switch(l::turnOn, l::turnOff);
s.flipUp(); // The light is on
s.flipDown(); // The light is off
Классы TurnOnLightCommand и TurnOffLightCommand нужны, когда команда хранит состояние или умеет больше одного действия, например execute() и undo().Где это применяется?
По цели ясно, что и для чего это нужно, а именно: в ситуации, когда нужно отделить конкретное исполнение, это очень удобно. Чтоб использование каких-то функций не зависело от конкретной реализации и ее можно было изменять без ущерба для системы. Типичные случаи, где это выручает:- отмена и повтор действий: команда помнит, как себя откатить, а история команд это обычный стек;
- очереди и планировщики задач: задачу кладут в очередь как объект и выполняют позже (так устроен
Runnableв JDK); - кнопки, пункты меню и горячие клавиши: одно действие привязано сразу к нескольким элементам интерфейса;
- макросы и скрипты: список команд, которые выполняются подряд;
- журналирование и транзакции: команды записывают в лог, а при сбое выполняют заново.
Builder, Singleton и прочие.Вопросы и ответы
Что такое паттерн Command простыми словами?
Это способ превратить вызов метода в объект. Вместо того чтобы вызыватьlight.turnOn() напрямую, создают объект-команду с методом execute(), внутри которого и спрятан этот вызов. Тот, кто держит команду, не знает, что она делает и с каким объектом: он просто вызывает execute(). Так запрос можно передавать, хранить и выполнять позже.
Кто такие receiver, invoker и client в паттерне Command?
Receiver (приемник) это объект, который умеет выполнять реальную работу, в примере классLight. Command (команда) хранит ссылку на приемник и в execute() вызывает нужный его метод: TurnOnLightCommand и TurnOffLightCommand. Invoker (вызывающий) хранит команды и запускает их, не зная деталей: класс Switch. Client (клиент) собирает все вместе: создает приемник и команды и передает их вызывающему, в примере это метод main класса TestCommand.
Зачем оборачивать вызов метода в объект, если можно вызвать его напрямую?
Прямой вызов жестко связывает того, кто просит действие, с тем, кто его выполняет:Switch должен был бы знать про Light и его методы. С командами Switch работает только с интерфейсом Command, поэтому к нему можно подключить любое действие, не меняя его код. Кроме того, объект-команду можно положить в список, очередь или историю, выполнить позже, повторить или отменить, а с обычным вызовом метода так не получится.
Где паттерн Command используется на практике?
Отмена и повтор действий в редакторах (каждая команда помнит, как себя откатить), очереди задач и планировщики (задача это команда, которую выполнят позже), кнопки и пункты меню в графических интерфейсах (одно действие привязано к нескольким элементам), макросы (список команд, выполняемых подряд), а также журналирование и транзакции. В JDK по этому принципу устроен интерфейсRunnable: объект с единственным методом run(), который поток выполняет, не зная, что внутри.
Чем Command отличается от Strategy?
Оба заворачивают поведение в объект с одним методом, но с разной целью. Strategy подставляет один из взаимозаменяемых алгоритмов для одной и той же задачи (например, разные способы сортировки), и выбирает его тот, кто эту задачу решает. Command заворачивает сам запрос: вызывающий не выбирает алгоритм и вообще не знает, что произойдет, а команды можно копить, откладывать и отменять. Подробнее про Strategy и State в статье о разнице между паттернами Состояние и Стратегия.Можно ли реализовать Command через лямбду в Java 8 и новее?
Да. ИнтерфейсCommand с единственным абстрактным методом execute() это функциональный интерфейс, поэтому вместо класса TurnOnLightCommand можно передать ссылку на метод: new Switch(light::turnOn, light::turnOff). Отдельные классы остаются полезны, когда команде нужно хранить состояние, например данные для отмены, или иметь несколько методов вроде execute() и undo().
См. также мои другие статьи:
Тестовое задание: "Написать Интерпретатор на язык BrainFuck"
Java: быстрее, сильнее и выше! Зарплаты украинских программистов
Как создать исполняемый jar в Intellij IDEA / how to create jar in IDEA
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ