Command Pattern — Команда

Запрос как объект: данные + действие. Можно сериализовать, передать, логировать, отменять. Основа CQRS и Event Sourcing.

Проблема

Действие в коде — императивный вызов: account.withdraw(100). Хочется:

  • логировать что именно произошло
  • сериализовать для очереди или БД
  • иметь возможность undo
  • передавать через сеть
  • параметризовать действия и ставить в очередь

Императивный вызов всё это не позволяет.

Где используется

  • Undo/Redo в текстовых редакторах и графических приложениях
  • Транзакции: выполнить серию операций или откатить все
  • Очереди задач: команды ставятся в очередь и выполняются поочерёдно
  • Макросы и пакетная обработка
  • Event Sourcing: хранение истории изменений как команд
  • Redux actions

Решение

Два варианта Command:

  1. Императивный — класс с методом execute. Содержит и данные, и действие.
  2. Анемичный — структура данных без методов (как 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: без undo Command — просто обёртка над функцией без преимуществ паттерна.
  • Команды с внешним состоянием: команда должна хранить всё необходимое для выполнения и отмены.
  • Путаница с Strategy: Strategy заменяет алгоритм; Command инкапсулирует запрос (с историей, отменой, очередью).

Главные тезисы автора

  • «Представление какой-то операции в виде объекта» — определение паттерна.
  • «Параметры и сам вызов как объект» — данные + действие вместе.
  • Анемичный объект = DTO — без методов, только данные, можно сериализовать.
  • Receiver/target — над кем выполняется команда; Invoker — кто запускает.
  • Команда + история = Event Sourcing. Команда + обратная команда = undo.
  • Команда — фундамент для CQS (Command Query Separation) и CQRS.

🎓 Источники

См. также