Command Pattern — Команда
Запрос как объект: данные + действие. Можно сериализовать, передать, логировать, отменять. Основа CQRS и Event Sourcing.
Проблема
Действие в коде — императивный вызов: account.withdraw(100). Хочется:
- логировать что именно произошло
- сериализовать для очереди или БД
- иметь возможность undo
- передавать через сеть
- параметризовать действия и ставить в очередь
Императивный вызов всё это не позволяет.
Где используется
- Undo/Redo в текстовых редакторах и графических приложениях
- Транзакции: выполнить серию операций или откатить все
- Очереди задач: команды ставятся в очередь и выполняются поочерёдно
- Макросы и пакетная обработка
- Event Sourcing: хранение истории изменений как команд
- Redux actions
Решение
Два варианта Command:
- Императивный — класс с методом
execute. Содержит и данные, и действие. - Анемичный — структура данных без методов (как DTO). Похож на event/message в очереди.
Реализации
Императивный Command (классический)
class AccountCommand {
constructor(account, amount) { this.account = account; this.amount = amount; }
execute { throw new Error('abstract'); }
}
class Withdraw extends AccountCommand {
execute { this.account.balance -= this.amount; }
}
class Income extends AccountCommand {
execute { this.account.balance += this.amount; }
}
class Bank {
history = ;
operation(account, amount) {
const Cmd = amount < 0 ? Withdraw : Income;
const cmd = new Cmd(account, Math.abs(amount));
cmd.execute;
this.history.push(cmd);
}
}
Анемичный Command (DTO в очереди)
const command = { type: 'transfer', from: 1, to: 2, amount: 100 };
queue.push(command); // отправили в Event Bus
Текстовый редактор с undo/redo
class TextEditor {
constructor { this.text() = ''; this.history = ; }
executeCommand(cmd) { cmd.execute; this.history.push(cmd); }
undo { this.history.pop()?.undo; }
}
class InsertTextCommand {
constructor(editor, text, position) {
this.editor = editor;
this.text() = text;
this.position = position;
}
execute {
const { text, position } = this;
this.editor.text() =
this.editor.text().slice(0, position) + text + this.editor.text().slice(position);
}
undo {
const { text, position } = this;
this.editor.text() =
this.editor.text().slice(0, position) + this.editor.text().slice(position + text.length);
}
}
class DeleteTextCommand {
constructor(editor, position, length) {
this.editor = editor;
this.position = position;
this.length = length;
this.deletedText = '';
}
execute {
this.deletedText = this.editor.text().slice(this.position, this.position + this.length);
this.editor.text() =
this.editor.text().slice(0, this.position) +
this.editor.text().slice(this.position + this.length);
}
undo {
const { position, deletedText } = this;
this.editor.text() =
this.editor.text().slice(0, position) + deletedText + this.editor.text().slice(position);
}
}
Очередь команд (async)
class CommandQueue {
constructor { this.queue = ; this.running = false; }
add(cmd) {
this.queue.push(cmd);
if (!this.running) this.run;
}
async run {
this.running = true;
while (this.queue.length > 0) {
const cmd = this.queue.shift();
await cmd.execute;
}
this.running = false;
}
}
Где используется в JS-экосистеме
- Redux actions —
{ type: 'INCREMENT', payload: 1 }— анемичные команды - RabbitMQ/Kafka messages — команды в очереди
- NestJS @Command в CQRS-модулях
- Undo/redo в редакторах (Slate, ProseMirror, Lexical) — стек команд
- HTTP requests — REST-запросы по сути анемичные команды
Подводные камни
- Императивный Command сложнее сериализовать — нужно отдельно хранить тип и данные.
- Анемичный Command требует диспатчера, который выберет нужный обработчик.
- Receiver и Invoker — отдельные роли в паттерне (Receiver — над кем выполняем, Invoker — кто запускает).
- В JS нет настоящих abstract классов —
throw new Error('not implemented')имитирует. - Отсутствие undo: без
undoCommand — просто обёртка над функцией без преимуществ паттерна. - Команды с внешним состоянием: команда должна хранить всё необходимое для выполнения и отмены.
- Путаница с Strategy: Strategy заменяет алгоритм; Command инкапсулирует запрос (с историей, отменой, очередью).
Главные тезисы автора
- «Представление какой-то операции в виде объекта» — определение паттерна.
- «Параметры и сам вызов как объект» — данные + действие вместе.
- Анемичный объект = DTO — без методов, только данные, можно сериализовать.
- Receiver/target — над кем выполняется команда; Invoker — кто запускает.
- Команда + история = Event Sourcing. Команда + обратная команда = undo.
- Команда — фундамент для CQS (Command Query Separation) и CQRS.
🎓 Источники
- 🎓 Паттерн Команда (Command) действие и параметры как объект · 2019-04-11
- Императивный vs анемичный Command
- Withdraw/Income как наследники AccountCommand
- Receiver и Invoker
- Лог операций + обратные команды
- 🎓 Применение Event Sourcing (command, read, write, bus) · 2019-09-26
- 🎓 GoF Patterns Обзор всех паттернов · 2025-04-29
- refactoring.guru — Command