BackendBit

К списку паттернов

Command – это объект, который инкапсулирует в себе все данные, необходимые для выполнения определённой операции.

Иногда возникает ситуация, когда объект-отправитель должен инициировать операцию, но не должен знать о конкретном получателе и деталях выполнения. Паттерн Command решает эту задачу: операция оборачивается в объект-команду, который хранит ссылку на получателя и вызывает нужный метод. Отправитель работает только с интерфейсом команды и вызывает execute(), ничего не зная о том, что происходит «под капотом».

UML диаграмма классов паттерна КомандаПоказывает интерфейс Command с методами execute и undo, конкретные команды TurnOnLight, TurnOffLight, RaiseTemperature, LowerTemperature, TurnOnAirConditioner и TurnOffAirConditioner, отправителя RemoteController и получателей Light и AirConditioner, организованные в три колонки: отправитель, команды и получатели.Отправитель (Invoker)Команды (Commands)Получатели (Receivers)RemoteController-buttons: Command[]-commandHistory: Deque<Command>+setCommand(int, Command): void+pressButton(int): void+pressUndoButton(): void<<interface>>Command+execute(): void+undo(): voidTurnOnLightTurnOffLightRaiseTemperatureLowerTemperatureTurnOnAirConditionerTurnOffAirConditionerLightAirConditioner

Пример

Представим, что у нас есть приложение для умного дома.

Пока что в умном доме у нас может быть два устройства: лампочка и кондиционер.

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...");
    }
}
java
package 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();
    }
}
java
package 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 инкапсулирует функцию, которую она выполняет. Корневая команда решает, какую конкретную команду запустить, на основе аргументов командной строки, но ничего не знает о логике внутри каждой команды.