Lapa Knowledge Base
Поведенческие паттерны

Command

Паттерн 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 — это инкапсуляция запросов в объекты!

Copyright © 2026