Command pattern для действий пользователя

Command pattern (или шаблон команд) — это поведенческий паттерн проектирования, который превращает запросы или простые операции в объекты. Он используется для инкапсуляции всех сведений, необходимых для выполнения действия, включая метод, параметры и объект. В контексте JavaScript и библиотеки Vex Command Pattern помогает организовать выполнение действий пользователя в виде независимых объектов, что упрощает их обработку и повторное использование.

Основные принципы Command Pattern

Шаблон Command представляет собой объект, который инкапсулирует все данные, связанные с конкретным действием. Этот объект обычно состоит из нескольких элементов:

  1. Команда (Command) — интерфейс с методом, который выполняет операцию.
  2. Конкретные команды (ConcreteCommand) — классы, реализующие интерфейс команды и определяющие действия.
  3. Приёмник (Receiver) — объект, который выполняет запрос.
  4. Invoker — объект, который инициирует выполнение команды.
  5. Клиент (Client) — объект, создающий команду и связывающий её с приёмником.
  6. Историк (History) — объект, который хранит выполненные команды для возможности их отката (если требуется).

Реализация Command Pattern в JavaScript

Для реализации шаблона команд в JavaScript можно использовать простую структуру объектов и классов. Рассмотрим пример, где приёмником будет объект, управляющий состоянием интерфейса, а команда — это действие пользователя.

Шаг 1: Создание интерфейса команды

Интерфейс команды содержит метод execute(), который будет вызван для выполнения действия.

class Command {
  execute() {
    throw new Error('Метод execute должен быть реализован');
  }
}

Шаг 2: Конкретные команды

Каждая команда будет реализовывать интерфейс Command и предоставлять конкретную реализацию метода execute. Например, команда для включения и выключения света.

class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOn();
  }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }

  execute() {
    this.light.turnOff();
  }
}

Шаг 3: Приёмник

Приёмник — это объект, который фактически выполняет операцию. В нашем случае это будет объект Light, управляющий состоянием света.

class Light {
  turnOn() {
    console.log('Свет включён');
  }

  turnOff() {
    console.log('Свет выключён');
  }
}

Шаг 4: Invoker

Invoker будет хранить и запускать команды. Он должен быть способен передавать команды для выполнения в ответ на действия пользователя.

class RemoteControl {
  constructor() {
    this.command = null;
  }

  setCommand(command) {
    this.command = command;
  }

  pressButton() {
    this.command.execute();
  }
}

Шаг 5: Клиент

Клиент создаёт объекты команд и связывает их с приёмниками. В данном примере создаются конкретные команды для управления светом и настраивается пульт управления.

const light = new Light();
const lightOn = new LightOnCommand(light);
const lightOff = new LightOffCommand(light);

const remote = new RemoteControl();

remote.setCommand(lightOn);
remote.pressButton();  // Свет включён

remote.setCommand(lightOff);
remote.pressButton();  // Свет выключён

Преимущества использования Command Pattern

  1. Инкапсуляция запросов. Команды можно легко передавать, хранить и изменять, не вмешиваясь в детали выполнения операции.
  2. Поддержка undo/redo. Легко реализовать механизм отмены и повторного выполнения операций, так как каждая команда является самостоятельным объектом с четко определённым действием.
  3. Упрощение добавления новых операций. Для добавления новых команд достаточно создать новый объект команды, не меняя существующую логику приёмников или инвокеров.
  4. Композиция команд. Команды можно комбинировать, создавая сложные действия из более простых. Например, можно создать команду, которая включает свет и затем запускает кондиционер.

Реализация undo/redo

Для добавления функционала отмены и повтора операций (undo/redo) нужно добавить поддержку истории команд в объект Invoker. История будет хранить выполненные команды, и их можно будет повторно вызвать в случае необходимости.

class RemoteControlWithUndo {
  constructor() {
    this.command = null;
    this.history = [];
  }

  setCommand(command) {
    this.command = command;
  }

  pressButton() {
    this.command.execute();
    this.history.push(this.command);
  }

  undo() {
    const command = this.history.pop();
    console.log('Отменено:');
    command.undo();
  }
}

class LightOnCommandWithUndo extends LightOnCommand {
  undo() {
    this.light.turnOff();
  }
}

class LightOffCommandWithUndo extends LightOffCommand {
  undo() {
    this.light.turnOn();
  }
}

Заключение

Использование Command Pattern в JavaScript позволяет эффективно управлять действиями пользователя, инкапсулируя запросы в объекты, что упрощает их обработку, добавление новых команд и управление состоянием. В библиотеке Vex, где часто требуются динамичные взаимодействия с пользователем, этот паттерн помогает создать гибкую и расширяемую архитектуру, минимизируя зависимость между различными частями приложения.