Command (Команда)
Command – это объект, который инкапсулирует в себе все данные, необходимые для выполнения определённой операции.
Иногда возникает ситуация, когда объект-отправитель должен инициировать операцию, но не должен знать о конкретном получателе и деталях выполнения. Паттерн Command решает эту задачу: операция оборачивается в объект-команду, который хранит ссылку на получателя и вызывает нужный метод. Отправитель работает только с интерфейсом команды и вызывает execute(), ничего не зная о том, что происходит «под капотом».
Пример
Представим, что у нас есть приложение для умного дома.
Пока что в умном доме у нас может быть два устройства: лампочка и кондиционер.
package smarthome;
public class Light {
public void turnOn() {
System.out.println("Turning the light on...");
}
public void turnOff() {
System.out.println("Turning the light off...");
}
}javapackage smarthome;
public class AirConditioner {
public void turnOn() {
System.out.println("Turning the air conditioner on...");
}
public void turnOff() {
System.out.println("Turning the air conditioner off...");
}
public void raiseTemperature() {
System.out.println("Raising the temperature...");
}
public void lowerTemperature() {
System.out.println("Lowering the temperature...");
}
}javaУмный дом управляется с помощью команд. Вот общий интерфейс для всех команд:
package smarthome;
public interface Command {
void execute();
}javaСоздадим конкретные команды для управления лампочкой:
package smarthome;
public final class TurnOnLight implements Command {
private final Light light;
public TurnOnLight(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}javapackage smarthome;
public final class TurnOffLight implements Command {
private final Light light;
public TurnOffLight(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}javaАналогичным образом создадим команду для повышения температуры кондиционера:
package smarthome;
public final class RaiseTemperature implements Command {
private final AirConditioner airConditioner;
public RaiseTemperature(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
}
@Override
public void execute() {
airConditioner.raiseTemperature();
}
}javaОстальные команды для кондиционера – TurnOnAirConditioner, TurnOffAirConditioner и LowerTemperature – устроены аналогично: каждая хранит ссылку на AirConditioner и вызывает соответствующий метод.
Заметим, что каждая команда содержит в себе ссылку на устройство, которому адресована команда. С точки зрения теории паттерна Command, наши устройства Light и AirConditioner являются получателями (Receivers).
Также создадим команду-пустышку, которая ничего не делает. Она пригодится в качестве значения по умолчанию для кнопок пульта:
package smarthome;
public final class NullCommand implements Command {
@Override
public void execute() {
// Ничего не делает
}
}javaДля управления нашим умным домом существует пульт с четырьмя кнопками. На каждую кнопку можно назначить любую команду:
package smarthome;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class RemoteController {
private static final int NUMBER_OF_BUTTONS = 4;
// По умолчанию подставляем команду-пустышку (null object)
private final List<Command> buttons = new ArrayList<>(
Collections.nCopies(NUMBER_OF_BUTTONS, new NullCommand())
);
public void setCommand(int button, Command command) {
checkButtonExists(button);
if (command == null) {
throw new IllegalArgumentException("Command must not be null");
}
buttons.set(button, command);
}
public void pressButton(int button) {
checkButtonExists(button);
buttons.get(button).execute();
}
private void checkButtonExists(int button) {
if (button < 0 || button >= NUMBER_OF_BUTTONS) {
throw new IndexOutOfBoundsException(
"Only buttons from 0 to " + (NUMBER_OF_BUTTONS - 1) + " are available"
);
}
}
}javaС точки зрения теории паттерна RemoteController (пульт) является отправителем (Invoker). Отправитель ничего не знает о конкретных получателях. Точно так же он ничего не знает о конкретных командах. Вместо этого он зависит от интерфейса Command и вызывает метод execute, когда нужно выполнить команду.
Light light = new Light();
AirConditioner ac = new AirConditioner();
RemoteController remote = new RemoteController();
remote.setCommand(0, new TurnOnLight(light));
remote.setCommand(1, new TurnOffLight(light));
remote.setCommand(2, new TurnOnAirConditioner(ac));
remote.setCommand(3, new RaiseTemperature(ac));
remote.pressButton(0); // Включаем свет...
remote.pressButton(3); // Повышаем температуру...javaЗа счёт того, что отправитель отделён от получателей и конкретных команд, мы можем добавлять новых получателей и новые команды без изменения кода отправителя.
Кроме этого можно добавлять в Invoker дополнительный общий функционал для всех команд. Например, мы хотим сделать на пульте отдельную кнопку, которая будет отменять последнюю вызванную команду.
Для этого нам нужно будет научить каждую команду выполнять операцию undo (отмена). Добавим эту операцию в интерфейс:
package undo;
public interface Command {
void execute();
void undo();
}javaДалее нужно реализовать эту операцию у каждой команды. Для примера покажем операцию undo для команды RaiseTemperature:
package undo;
public final class RaiseTemperature implements Command {
private final AirConditioner airConditioner;
public RaiseTemperature(AirConditioner airConditioner) {
this.airConditioner = airConditioner;
}
@Override
public void execute() {
airConditioner.raiseTemperature();
}
@Override
public void undo() {
// Чтобы отменить повышение температуры, нужно понизить её
airConditioner.lowerTemperature();
}
}javaКоманде-пустышке тоже нужно реализовать undo – она по-прежнему ничего не делает:
package undo;
public final class NullCommand implements Command {
@Override
public void execute() {
// Ничего не делает
}
@Override
public void undo() {
// Ничего не делает
}
}javaВ RemoteController нам нужно хранить историю последних выполненных команд, чтобы знать, какие команды отменять:
package undo;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
public final class RemoteController {
private static final int NUMBER_OF_BUTTONS = 4;
private final List<Command> buttons = new ArrayList<>(
Collections.nCopies(NUMBER_OF_BUTTONS, new NullCommand())
);
private final Deque<Command> commandHistory = new ArrayDeque<>();
public void setCommand(int button, Command command) {
checkButtonExists(button);
if (command == null) {
throw new IllegalArgumentException("Command must not be null");
}
buttons.set(button, command);
}
public void pressButton(int button) {
checkButtonExists(button);
Command command = buttons.get(button);
command.execute();
// После выполнения команды добавляем её в историю
commandHistory.push(command);
}
public void pressUndoButton() {
if (commandHistory.isEmpty()) {
return;
}
// Достаём последнюю команду из истории
Command command = commandHistory.pop();
// Вызываем операцию отмены
command.undo();
}
private void checkButtonExists(int button) {
if (button < 0 || button >= NUMBER_OF_BUTTONS) {
throw new IndexOutOfBoundsException(
"Only buttons from 0 to " + (NUMBER_OF_BUTTONS - 1) + " are available"
);
}
}
}javaРеальные примеры
java.lang.Runnable
Интерфейс Runnable – это по сути команда с единственным методом run(), который аналогичен execute(). Задачи (Runnable) передаются в ExecutorService, который выступает в роли Invoker: он принимает и выполняет команды, ничего не зная об их конкретном содержании.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Runnable sendEmail = () -> {
// Отправка email
System.out.println("Sending email...");
};
Runnable generateReport = () -> {
// Генерация отчёта
System.out.println("Generating report...");
};
// ExecutorService – Invoker: выполняет команды, не зная их конкретного содержания
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(sendEmail);
executor.execute(generateReport);
executor.shutdown();javaКаждый экземпляр Runnable инкапсулирует данные, необходимые для выполнения операции (через замыкание или поля класса). ExecutorService зависит только от интерфейса Runnable и не знает, что именно делает каждая задача.
cobra.Command
Популярный Go-фреймворк для CLI-приложений cobra использует паттерн Command. Каждая CLI-команда представлена структурой cobra.Command с полем Run – аналогом execute(). Корневая команда выступает в роли Invoker.
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func main() {
// Конкретная команда: очистка кэша
clearCacheCmd := &cobra.Command{
Use: "clear-cache",
Short: "Очистить кэш приложения",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Cache cleared")
},
}
// Конкретная команда: миграция базы данных
migrateCmd := &cobra.Command{
Use: "migrate",
Short: "Применить миграции базы данных",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Migrations applied")
},
}
// Root command – Invoker: запускает переданные команды
rootCmd := &cobra.Command{Use: "app"}
rootCmd.AddCommand(clearCacheCmd)
rootCmd.AddCommand(migrateCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}goКаждая cobra.Command инкапсулирует функцию, которую она выполняет. Корневая команда решает, какую конкретную команду запустить, на основе аргументов командной строки, но ничего не знает о логике внутри каждой команды.