Command
Command — это поведенческий паттерн проектирования, который превращает запросы в объекты, позволяя передавать их как аргументы при вызове методов, ставить запросы в очередь, логировать их, а также поддерживать отмену операций.
Проблема
Представьте, что вы создаете приложение для работы с текстом. Пользователи могут выполнять различные операции: создавать документы, открывать файлы, сохранять изменения, копировать текст, вставлять текст и так далее.
Вы хотите добавить панель инструментов с кнопками для каждой операции. Также вы хотите добавить поддержку горячих клавиш и возможность отмены операций.
Первое, что приходит в голову, — это создать класс для каждой операции и связать кнопки с соответствующими методами. Но это создаст тесную связанность между кнопками и операциями.
Решение
Паттерн Command предлагает создать объекты для операций вместо прямых вызовов методов. Каждый такой объект содержит всю информацию, необходимую для выполнения операции.
Структура
Command
└── execute(): void
ConcreteCommand
├── receiver: Receiver
├── execute(): void
└── undo(): void
Invoker
├── commands: Command[]
├── executeCommand(command: Command): void
└── undoLastCommand(): void
Receiver
└── action(): void
Пример реализации
JavaScript
// Интерфейс команды
class Command {
execute() {
throw new Error('Метод execute должен быть переопределен');
}
undo() {
throw new Error('Метод undo должен быть переопределен');
}
}
// Получатель команд - текстовый редактор
class TextEditor {
constructor() {
this.content = '';
this.cursorPosition = 0;
}
insertText(text, position) {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
this.cursorPosition = position + text.length;
console.log(`Вставлен текст "${text}" в позицию ${position}`);
}
deleteText(start, length) {
const deletedText = this.content.slice(start, start + length);
this.content = this.content.slice(0, start) + this.content.slice(start + length);
this.cursorPosition = start;
console.log(`Удален текст "${deletedText}" с позиции ${start}`);
return deletedText;
}
getContent() {
return this.content;
}
getCursorPosition() {
return this.cursorPosition;
}
}
// Конкретные команды
class InsertTextCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insertText(this.text, this.position);
}
undo() {
const start = this.position;
const length = this.text.length;
this.editor.deleteText(start, length);
}
}
class DeleteTextCommand extends Command {
constructor(editor, start, length) {
super();
this.editor = editor;
this.start = start;
this.length = length;
this.deletedText = '';
}
execute() {
this.deletedText = this.editor.deleteText(this.start, this.length);
}
undo() {
this.editor.insertText(this.deletedText, this.start);
}
}
class CopyCommand extends Command {
constructor(editor, start, length) {
super();
this.editor = editor;
this.start = start;
this.length = length;
this.copiedText = '';
}
execute() {
this.copiedText = this.editor.getContent().slice(this.start, this.start + this.length);
console.log(`Скопирован текст: "${this.copiedText}"`);
}
undo() {
// Копирование не изменяет состояние, поэтому отмена не нужна
console.log('Отмена копирования не требуется');
}
getCopiedText() {
return this.copiedText;
}
}
class PasteCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insertText(this.text, this.position);
}
undo() {
const start = this.position;
const length = this.text.length;
this.editor.deleteText(start, length);
}
}
// Вызыватель команд - менеджер команд
class CommandManager {
constructor() {
this.history = [];
this.currentIndex = -1;
}
executeCommand(command) {
// Удаляем команды после текущей позиции (если есть)
this.history = this.history.slice(0, this.currentIndex + 1);
// Выполняем команду
command.execute();
// Добавляем в историю
this.history.push(command);
this.currentIndex++;
console.log(`Команда выполнена. История: ${this.history.length} команд`);
}
undo() {
if (this.currentIndex >= 0) {
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
console.log(`Команда отменена. Текущая позиция: ${this.currentIndex}`);
} else {
console.log('Нет команд для отмены');
}
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
console.log(`Команда повторена. Текущая позиция: ${this.currentIndex}`);
} else {
console.log('Нет команд для повтора');
}
}
getHistory() {
return this.history.slice(0, this.currentIndex + 1);
}
}
// Использование
const editor = new TextEditor();
const commandManager = new CommandManager();
console.log('=== Работа с текстовым редактором ===');
// Выполняем команды
const insertCmd1 = new InsertTextCommand(editor, 'Привет, ', 0);
commandManager.executeCommand(insertCmd1);
const insertCmd2 = new InsertTextCommand(editor, 'мир!', editor.getCursorPosition());
commandManager.executeCommand(insertCmd2);
const copyCmd = new CopyCommand(editor, 0, 6);
commandManager.executeCommand(copyCmd);
const pasteCmd = new PasteCommand(editor, copyCmd.getCopiedText(), editor.getCursorPosition());
commandManager.executeCommand(pasteCmd);
console.log(`\nТекущий текст: "${editor.getContent()}"`);
// Отменяем команды
console.log('\n=== Отмена команд ===');
commandManager.undo();
commandManager.undo();
commandManager.undo();
console.log(`Текст после отмены: "${editor.getContent()}"`);
// Повторяем команды
console.log('\n=== Повтор команд ===');
commandManager.redo();
commandManager.redo();
console.log(`Текст после повтора: "${editor.getContent()}"`);
TypeScript с интерфейсами
// Интерфейс команды
interface Command {
execute(): void;
undo(): void;
}
// Получатель - система управления освещением
class Light {
private isOn: boolean = false;
private brightness: number = 0;
turnOn(): void {
this.isOn = true;
this.brightness = 100;
console.log('Свет включен');
}
turnOff(): void {
this.isOn = false;
this.brightness = 0;
console.log('Свет выключен');
}
setBrightness(level: number): void {
this.brightness = Math.max(0, Math.min(100, level));
console.log(`Яркость установлена на ${this.brightness}%`);
}
getBrightness(): number {
return this.brightness;
}
isLightOn(): boolean {
return this.isOn;
}
}
// Конкретные команды
class TurnOnLightCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
class TurnOffLightCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOff();
}
undo(): void {
this.light.turnOn();
}
}
class DimLightCommand implements Command {
private light: Light;
private previousBrightness: number;
private newBrightness: number;
constructor(light: Light, brightness: number) {
this.light = light;
this.newBrightness = brightness;
this.previousBrightness = light.getBrightness();
}
execute(): void {
this.light.setBrightness(this.newBrightness);
}
undo(): void {
this.light.setBrightness(this.previousBrightness);
}
}
// Макрокоманда
class MacroCommand implements Command {
private commands: Command[] = [];
addCommand(command: Command): void {
this.commands.push(command);
}
execute(): void {
console.log('Выполнение макрокоманды...');
for (const command of this.commands) {
command.execute();
}
}
undo(): void {
console.log('Отмена макрокоманды...');
for (let i = this.commands.length - 1; i >= 0; i--) {
this.commands[i].undo();
}
}
}
// Вызыватель - пульт управления
class RemoteControl {
private commands: Map<string, Command> = new Map();
private lastCommand: Command | null = null;
setCommand(button: string, command: Command): void {
this.commands.set(button, command);
}
pressButton(button: string): void {
const command = this.commands.get(button);
if (command) {
command.execute();
this.lastCommand = command;
} else {
console.log(`Кнопка "${button}" не настроена`);
}
}
pressUndo(): void {
if (this.lastCommand) {
this.lastCommand.undo();
this.lastCommand = null;
} else {
console.log('Нет команды для отмены');
}
}
}
// Использование
const livingRoomLight = new Light();
const bedroomLight = new Light();
const remote = new RemoteControl();
// Настраиваем команды
remote.setCommand('on', new TurnOnLightCommand(livingRoomLight));
remote.setCommand('off', new TurnOffLightCommand(livingRoomLight));
remote.setCommand('dim', new DimLightCommand(livingRoomLight, 30));
// Создаем макрокоманду для вечернего режима
const eveningMode = new MacroCommand();
eveningMode.addCommand(new TurnOnLightCommand(livingRoomLight));
eveningMode.addCommand(new DimLightCommand(livingRoomLight, 20));
eveningMode.addCommand(new TurnOnLightCommand(bedroomLight));
eveningMode.addCommand(new DimLightCommand(bedroomLight, 10));
remote.setCommand('evening', eveningMode);
console.log('=== Управление освещением ===');
// Используем пульт
remote.pressButton('on');
remote.pressButton('dim');
remote.pressButton('evening');
console.log('\n=== Отмена команд ===');
remote.pressUndo();
remote.pressUndo();
Когда использовать
Используйте Command, когда:
- Нужно параметризовать объекты операциями
- Нужно ставить операции в очередь, выполнять их по расписанию или передавать по сети
- Нужна поддержка отмены операций
- Нужно логировать операции
- Нужно поддерживать макрокоманды
Преимущества
- Разделяет класс, который вызывает операцию, от класса, который её выполняет
- Позволяет параметризовать объекты операциями
- Поддерживает отмену операций
- Поддерживает макрокоманды
- Легко добавлять новые команды
Недостатки
- Увеличивает количество классов в приложении
- Может усложнить код для простых операций
- Требует дополнительной памяти для хранения команд
Отличия от других паттернов
- Strategy — выбирает алгоритм во время выполнения
- State — изменяет поведение при изменении состояния
- Chain of Responsibility — передает запросы по цепочке
Реальный пример
// Command для системы управления задачами
class TaskCommand {
constructor(taskManager, task) {
this.taskManager = taskManager;
this.task = task;
}
execute() {
throw new Error('Метод execute должен быть переопределен');
}
undo() {
throw new Error('Метод undo должен быть переопределен');
}
}
class CreateTaskCommand extends TaskCommand {
execute() {
this.taskManager.addTask(this.task);
console.log(`Создана задача: ${this.task.title}`);
}
undo() {
this.taskManager.removeTask(this.task.id);
console.log(`Отменено создание задачи: ${this.task.title}`);
}
}
class CompleteTaskCommand extends TaskCommand {
execute() {
this.taskManager.completeTask(this.task.id);
console.log(`Задача выполнена: ${this.task.title}`);
}
undo() {
this.taskManager.uncompleteTask(this.task.id);
console.log(`Отменено выполнение задачи: ${this.task.title}`);
}
}
class DeleteTaskCommand extends TaskCommand {
constructor(taskManager, task) {
super(taskManager, task);
this.wasCompleted = task.completed;
}
execute() {
this.taskManager.removeTask(this.task.id);
console.log(`Задача удалена: ${this.task.title}`);
}
undo() {
this.taskManager.addTask({ ...this.task, completed: this.wasCompleted });
console.log(`Отменено удаление задачи: ${this.task.title}`);
}
}
// Использование
const taskManager = new TaskManager();
const commandHistory = [];
// Создаем и выполняем команды
const createCmd = new CreateTaskCommand(taskManager, { id: 1, title: 'Купить молоко', completed: false });
createCmd.execute();
commandHistory.push(createCmd);
const completeCmd = new CompleteTaskCommand(taskManager, { id: 1, title: 'Купить молоко', completed: false });
completeCmd.execute();
commandHistory.push(completeCmd);
// Отменяем последнюю команду
if (commandHistory.length > 0) {
const lastCommand = commandHistory.pop();
lastCommand.undo();
}
Command — это инкапсуляция запросов в объекты!