Menu
Coddy logo textTech

Паттерн Команда

Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 96 из 104.

Паттерн Команда (Command) инкапсулирует запрос в виде объекта, позволяя параметризовать клиентов различными запросами, ставить операции в очередь или поддерживать функциональность отмены. Он отделяет объект, инициирующий операцию, от объекта, который непосредственно знает, как её выполнить.

Паттерн состоит из четырех ключевых компонентов: интерфейс Command с методом execute(), Concrete Commands, которые реализуют определенные действия, Receiver, который выполняет фактическую работу, и Invoker, который запускает команды:

#include <iostream>
#include <memory>
#include <vector>

// Получатель (Receiver) — знает, как выполнять операции
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// Интерфейс команды
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Конкретные команды
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

class LightOffCommand : public Command {
    Light& light;
public:
    LightOffCommand(Light& l) : light(l) {}
    void execute() override { light.turnOff(); }
    void undo() override { light.turnOn(); }
};

// Инициатор (Invoker)
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

Каждая команда хранит ссылку на своего получателя и вызывает соответствующий метод при выполнении. Инициатор не знает, какое действие произойдет — он просто вызывает execute(). Сохраняя команды в истории, вы можете легко реализовать отмену, вызывая метод undo() каждой команды.

Используйте паттерн Команда, когда вам нужно ставить операции в очередь, реализовать отмену/повтор действий или отделить отправителя запроса от его обработчика.

challenge icon

Задание

Легко

Давайте создадим Текстовый редактор с функцией отмены (undo), используя паттерн Команда (Command). Вы создадите систему, в которой операции редактирования (такие как вставка и удаление текста) инкапсулированы в объекты команд, что позволяет пользователям выполнять действия, а затем отменять их в обратном порядке — совсем как в настоящем текстовом редакторе.

Вы организуете свой код в четырех файлах:

  • TextDocument.h: Создайте класс-получатель (receiver), который хранит и манипулирует фактическим текстовым содержимым.

    Ваш класс TextDocument должен хранить содержимое документа в виде строки. Реализуйте следующие методы:

    • insertText(const std::string& text) — добавляет текст в конец содержимого
    • deleteText(int count) — удаляет последние count символов из содержимого (ничего не делайте, если count превышает длину содержимого)
    • getContent() — возвращает текущее содержимое как константную ссылку
  • Command.h: Определите интерфейс команды и конкретные классы команд.

    Создайте абстрактный класс Command с чисто виртуальными методами execute() и undo(), а также виртуальным деструктором.

    Затем реализуйте две конкретные команды:

    • InsertCommand — принимает ссылку на TextDocument и строку для вставки. При выполнении вставляет текст. При отмене удаляет то же количество символов, которое было вставлено.
    • DeleteCommand — принимает ссылку на TextDocument и количество символов для удаления. Она должна сохранять удаленный текст, чтобы иметь возможность восстановить его при отмене. При выполнении удаляет символы с конца. При отмене вставляет сохраненный текст обратно.
  • TextEditor.h: Создайте инициатор (invoker), который управляет выполнением команд и историей.

    Ваш класс TextEditor должен хранить ссылку на TextDocument и вести историю выполненных команд, используя вектор unique_ptr<Command>. Реализуйте:

    • executeCommand(std::unique_ptr<Command> cmd) — выполняет команду и добавляет ее в историю
    • undo() — отменяет последнюю команду и удаляет ее из истории (ничего не делайте, если история пуста)
    • showContent() — выводит текущее содержимое документа или [empty], если содержимое пусто
  • main.cpp: Продемонстрируйте работу паттерна Команда с функцией отмены.

    Прочитайте три входных значения:

    1. Первый текст для вставки (строка)
    2. Второй текст для вставки (строка)
    3. Количество символов для удаления (целое число)

    Создайте TextDocument и TextEditor. Затем выполните следующие операции по порядку, отображая содержимое после каждого шага:

    1. Вставьте первый текст, затем покажите содержимое
    2. Вставьте второй текст, затем покажите содержимое
    3. Удалите указанное количество символов, затем покажите содержимое
    4. Выполните отмену один раз, затем покажите содержимое
    5. Выполните отмену еще раз, затем покажите содержимое

Например, при входных данных Hello, World и 3:

Hello
Hello World
Hello Wo
Hello World
Hello

При входных данных Code, Editor и 6:

Code
CodeEditor
Code
CodeEditor
Code

Обратите внимание, как каждая команда знает, как отменить саму себя. DeleteCommand запоминает, что она удалила, чтобы иметь возможность это восстановить, а InsertCommand точно знает, сколько символов нужно удалить при отмене. В этом и заключается сила паттерна Команда — операции становятся объектами первого класса, которые можно хранить, выполнять и отменять.

Шпаргалка

Паттерн Команда (Command) инкапсулирует запрос в виде объекта, позволяя параметризовать клиенты различными запросами, ставить операции в очередь или поддерживать функциональность отмены. Он отделяет объект, инициирующий операцию, от объекта, который знает, как её выполнить.

Паттерн состоит из четырех ключевых компонентов:

  • Интерфейс Command с методом execute()
  • Конкретные команды (Concrete Commands), которые реализуют определенные действия
  • Получатель (Receiver), который выполняет фактическую работу
  • Инициатор (Invoker), который запускает команды

Базовая структура:

// Получатель — знает, как выполнять операции
class Light {
public:
    void turnOn() { std::cout << "Light is ON\n"; }
    void turnOff() { std::cout << "Light is OFF\n"; }
};

// Интерфейс команды
class Command {
public:
    virtual void execute() = 0;
    virtual void undo() = 0;
    virtual ~Command() = default;
};

// Конкретная команда
class LightOnCommand : public Command {
    Light& light;
public:
    LightOnCommand(Light& l) : light(l) {}
    void execute() override { light.turnOn(); }
    void undo() override { light.turnOff(); }
};

// Инициатор
class RemoteControl {
    std::vector<std::unique_ptr<Command>> history;
public:
    void pressButton(std::unique_ptr<Command> cmd) {
        cmd->execute();
        history.push_back(std::move(cmd));
    }
    
    void pressUndo() {
        if (!history.empty()) {
            history.back()->undo();
            history.pop_back();
        }
    }
};

Каждая команда хранит ссылку на своего получателя и вызывает соответствующий метод при выполнении. Инициатор не знает, какое действие произойдет — он просто вызывает execute(). Сохраняя команды в истории, вы можете реализовать отмену, вызывая метод undo() каждой команды.

Используйте паттерн Команда, когда вам нужно ставить операции в очередь, реализовать отмену/повтор действий или отделить отправителя запроса от его обработчика.

Попробуйте сами

#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"

int main() {
    // Чтение входных данных
    std::string firstText;
    std::string secondText;
    int deleteCount;
    
    std::getline(std::cin, firstText);
    std::getline(std::cin, secondText);
    std::cin >> deleteCount;

    // TODO: Создайте TextDocument и TextEditor
    
    // TODO: Выполните операции по порядку, отображая содержимое после каждой:
    // 1. Вставьте первый текст, затем выведите содержимое
    // 2. Вставьте второй текст, затем выведите содержимое
    // 3. Удалите указанное количество символов, затем выведите содержимое
    // 4. Отмените действие один раз, затем выведите содержимое
    // 5. Отмените действие еще раз, затем выведите содержимое
    
    // Подсказка: Используйте std::make_unique<InsertCommand>(...) и 
    //       std::make_unique<DeleteCommand>(...) для создания команд

    return 0;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование