Операции как объекты

Паттерн «команда» инкапсулирует операцию как объект. Вместо прямого вызова функции, вызывающий создаёт объект команды и передаёт его исполнителю. Исполнитель решает, когда (и нужно ли) его выполнять.

Это позволяет:

  • Отмену/повтор — каждая команда знает, как отменить себя
  • Запись макросов — собирать команды для воспроизведения
  • Отложенное выполнение — ставить команды в очередь на потом
  • Логирование — записывать команды по мере выполнения
  • Откат транзакций — выполнить список; при ошибке — откатить все

Базовый интерфейс команды

1class ICommand {
2public:
3    virtual void execute() = 0;
4    virtual void undo()    = 0;
5    virtual ~ICommand()    = default;
6};

Конкретная команда захватывает получателя (объект, над которым она работает) и любые параметры в своём конструкторе:

 1class SetTemperatureCommand : public ICommand {
 2    Thermostat& thermostat_;
 3    float       newTemp_;
 4    float       prevTemp_;   // сохраняется для отмены
 5
 6public:
 7    SetTemperatureCommand(Thermostat& t, float temp)
 8        : thermostat_(t), newTemp_(temp) {}
 9
10    void execute() override {
11        prevTemp_ = thermostat_.getSetPoint();
12        thermostat_.setSetPoint(newTemp_);
13    }
14
15    void undo() override {
16        thermostat_.setSetPoint(prevTemp_);
17    }
18};

Использование:

1auto cmd = std::make_unique<SetTemperatureCommand>(thermostat, 23.0f);
2cmd->execute();

Стек отмены/повтора

 1class CommandHistory {
 2    std::vector<std::unique_ptr<ICommand>> done_;
 3    std::vector<std::unique_ptr<ICommand>> undone_;
 4
 5public:
 6    void execute(std::unique_ptr<ICommand> cmd) {
 7        cmd->execute();
 8        done_.push_back(std::move(cmd));
 9        undone_.clear();   // новое действие инвалидирует стек повтора
10    }
11
12    bool canUndo() const { return !done_.empty(); }
13    bool canRedo() const { return !undone_.empty(); }
14
15    void undo() {
16        if (done_.empty()) return;
17        auto cmd = std::move(done_.back());
18        done_.pop_back();
19        cmd->undo();
20        undone_.push_back(std::move(cmd));
21    }
22
23    void redo() {
24        if (undone_.empty()) return;
25        auto cmd = std::move(undone_.back());
26        undone_.pop_back();
27        cmd->execute();
28        done_.push_back(std::move(cmd));
29    }
30};

Использование:

1CommandHistory history;
2
3history.execute(std::make_unique<SetTemperatureCommand>(t, 23.0f));
4history.execute(std::make_unique<SetTemperatureCommand>(t, 25.0f));
5// термостат на 25°C
6
7history.undo();   // возврат к 23°C
8history.undo();   // возврат к исходному
9history.redo();   // вперёд к 23°C

Интеграция с Qt — QUndoCommand

Qt предоставляет готовый паттерн «команда» в QUndoStack / QUndoCommand. Добавляет: строки описания (отображаемые в меню «Правка → Отменить»), слияние команд (группировка быстрых последовательных изменений), автоматическую привязку к действиям меню.

 1#include <QUndoCommand>
 2#include <QUndoStack>
 3
 4class SetThermostatCommand : public QUndoCommand {
 5    Thermostat* thermostat_;
 6    float       newTemp_;
 7    float       prevTemp_;
 8
 9public:
10    SetThermostatCommand(Thermostat* t, float temp, QUndoCommand* parent = nullptr)
11        : QUndoCommand(QString("Установить температуру %1°C").arg(temp), parent)
12        , thermostat_(t), newTemp_(temp) {}
13
14    void redo() override {
15        prevTemp_ = thermostat_->setPoint();
16        thermostat_->setSetPoint(newTemp_);
17    }
18
19    void undo() override {
20        thermostat_->setSetPoint(prevTemp_);
21    }
22
23    // Объединить последовательные команды установки температуры в один шаг отмены
24    bool mergeWith(const QUndoCommand* other) override {
25        if (other->id() != id()) return false;
26        newTemp_ = static_cast<const SetThermostatCommand*>(other)->newTemp_;
27        return true;
28    }
29
30    int id() const override { return 42; }   // уникальный ID для слияния
31};

Подключение к UI:

 1// В виджете
 2QUndoStack* undoStack = new QUndoStack(this);
 3
 4// Привязать к действиям меню — Qt управляет текстом и состоянием включения
 5QAction* undoAction = undoStack->createUndoAction(this, tr("&Отменить"));
 6QAction* redoAction = undoStack->createRedoAction(this, tr("&Повторить"));
 7undoAction->setShortcut(QKeySequence::Undo);
 8redoAction->setShortcut(QKeySequence::Redo);
 9
10// Выполнить через стек
11void MainWindow::onSliderChanged(int value) {
12    float temp = value / 10.0f;
13    undoStack->push(new SetThermostatCommand(thermostat_, temp));
14}

В меню отмены отображается «Отменить Установить температуру 23.0°C» — строка описания команды. Быстрые движения слайдера объединяются в один шаг отмены.


Очередь отложенного выполнения

Команды упрощают откладывание операций на потом:

 1class CommandQueue {
 2    std::queue<std::unique_ptr<ICommand>> queue_;
 3    std::mutex mtx_;
 4
 5public:
 6    void enqueue(std::unique_ptr<ICommand> cmd) {
 7        std::lock_guard lk(mtx_);
 8        queue_.push(std::move(cmd));
 9    }
10
11    void executeAll() {
12        std::queue<std::unique_ptr<ICommand>> local;
13        {
14            std::lock_guard lk(mtx_);
15            local.swap(queue_);
16        }
17        while (!local.empty()) {
18            local.front()->execute();
19            local.pop();
20        }
21    }
22};

Использование: поток UI ставит команды в очередь; поток устройства сливает очередь в своём главном цикле. Никаких прямых межпоточных вызовов к состоянию устройства.

 1// Поток UI — ставит в очередь безопасно
 2queue.enqueue(std::make_unique<SetTemperatureCommand>(device, 22.5f));
 3
 4// Поток устройства — выполняет в своём контексте
 5void deviceLoop() {
 6    while (running) {
 7        queue.executeAll();
 8        device.update();
 9        HAL_Delay(10);
10    }
11}

Запись макросов

Записать последовательность команд для воспроизведения:

 1class MacroCommand : public ICommand {
 2    std::vector<std::unique_ptr<ICommand>> steps_;
 3public:
 4    void add(std::unique_ptr<ICommand> step) {
 5        steps_.push_back(std::move(step));
 6    }
 7
 8    void execute() override {
 9        for (auto& s : steps_) s->execute();
10    }
11
12    void undo() override {
13        for (auto it = steps_.rbegin(); it != steps_.rend(); ++it)
14            (*it)->undo();
15    }
16};
17
18// Запись сессии
19auto macro = std::make_unique<MacroCommand>();
20macro->add(std::make_unique<SetTemperatureCommand>(t, 20.0f));
21macro->add(std::make_unique<SetFanSpeedCommand>(fan, 50));
22macro->add(std::make_unique<EnableAlarmCommand>(alarm, true));
23
24// Выполнить или добавить в историю как одно целое
25history.execute(std::move(macro));
26history.undo();   // откатывает все три шага

Команда на основе лямбд (без иерархии классов)

Для простых случаев без отмены достаточно пары лямбд:

 1struct Command {
 2    std::function<void()> doIt;
 3    std::function<void()> undoIt;
 4};
 5
 6class SimpleHistory {
 7    std::vector<Command> history_;
 8    size_t current_ = 0;
 9
10public:
11    void execute(Command cmd) {
12        history_.resize(current_);   // очистить повтор
13        cmd.doIt();
14        history_.push_back(std::move(cmd));
15        ++current_;
16    }
17
18    void undo() {
19        if (current_ == 0) return;
20        history_[--current_].undoIt();
21    }
22
23    void redo() {
24        if (current_ >= history_.size()) return;
25        history_[current_++].doIt();
26    }
27};
28
29// Использование — никаких классов писать не нужно
30float prevTemp = thermostat.setPoint();
31history.execute({
32    [&] { thermostat.setSetPoint(23.0f); },
33    [&, prevTemp] { thermostat.setSetPoint(prevTemp); }
34});

Удобно, когда типов команд мало и логика отмены тривиальна.


Итоги

  • Паттерн «команда»: инкапсулировать операцию и её обратное действие как объект
  • Стек отмены/повтора: добавлять выполненные команды; извлекать для отмены, добавлять обратно для повтора; очищать повтор при новом действии
  • Qt: QUndoCommand + QUndoStack — строки описания, слияние, привязка действий меню
  • Отложенная очередь: поток UI ставит в очередь, поток устройства сливает — безопасная последовательность между потоками
  • Макрос: MacroCommand оборачивает список команд — выполнять и отменять как один атомарный шаг
  • Вариант с лямбдами: пара {doIt, undoIt} для простых случаев без иерархии классов

Что дальше