Операции как объекты
Паттерн «команда» инкапсулирует операцию как объект. Вместо прямого вызова функции, вызывающий создаёт объект команды и передаёт его исполнителю. Исполнитель решает, когда (и нужно ли) его выполнять.
Это позволяет:
- Отмену/повтор — каждая команда знает, как отменить себя
- Запись макросов — собирать команды для воспроизведения
- Отложенное выполнение — ставить команды в очередь на потом
- Логирование — записывать команды по мере выполнения
- Откат транзакций — выполнить список; при ошибке — откатить все
Базовый интерфейс команды
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}для простых случаев без иерархии классов
Что дальше
- Паттерн «наблюдатель» — уведомление UI при изменении состояния командами
- Паттерн «активный объект» — очередь команд как очередь сообщений активного объекта
- Паттерны «фабрика» и «строитель» — создание объектов команд