Введение
Большинство интерактивных приложений (текстовые редакторы, графические редакторы) нуждаются в функциональности отмены/повтора действий (undo/redo) — Command решает эту задачу, превращая каждое отдельное действие пользователя в самостоятельный объект, который можно сохранить в истории, выполнить, и, что особенно важно, отменить позже.
Концепция
Command инкапсулирует запрос на выполнение определённого действия (включая всю информацию, необходимую для его выполнения) как самостоятельный объект с единым интерфейсом (обычно — методы execute() и undo()) — вместо непосредственного, прямого вызова метода, выполняющего действие, код создаёт объект команды и передаёт его инициатору выполнения (часто называемому Invoker), который вызывает execute() для реального выполнения и может сохранить выполненную команду в истории для последующего, возможного вызова undo(), отменяющего это конкретное действие.
Пример кода
#include <iostream>
#include <memory>
#include <vector>
#include <string>
class Document
{
public:
void insertText(const std::string &text, size_t position) { m_content.insert(position, text); }
void deleteText(size_t position, size_t length) { m_content.erase(position, length); }
const std::string &content() const { return m_content; }
private:
std::string m_content;
};
class Command
{
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
class InsertTextCommand : public Command // КОНКРЕТНАЯ команда — хранит ВСЮ информацию для execute()/undo()
{
public:
InsertTextCommand(Document &doc, std::string text, size_t pos)
: m_document(doc), m_text(std::move(text)), m_position(pos) {}
void execute() override { m_document.insertText(m_text, m_position); }
void undo() override { m_document.deleteText(m_position, m_text.length()); } // ОБРАТНАЯ операция
private:
Document &m_document;
std::string m_text;
size_t m_position;
};
class CommandHistory // Invoker — выполняет команды И хранит историю для undo/redo
{
public:
void executeCommand(std::unique_ptr<Command> command)
{
command->execute();
m_history.push_back(std::move(command)); // сохранение для ВОЗМОЖНОГО последующего undo
}
void undoLast()
{
if (m_history.empty()) return;
m_history.back()->undo();
m_history.pop_back();
}
private:
std::vector<std::unique_ptr<Command>> m_history;
};
int main()
{
Document doc;
CommandHistory history;
history.executeCommand(std::make_unique<InsertTextCommand>(doc, "Привет", 0));
history.executeCommand(std::make_unique<InsertTextCommand>(doc, ", мир", 6));
std::cout << doc.content() << std::endl; // "Привет, мир"
history.undoLast(); // отмена ВТОРОЙ команды
std::cout << doc.content() << std::endl; // "Привет" — ", мир" корректно отменено
return 0;
}
Пояснения к коду
InsertTextCommand, хранящий m_document, m_text, m_position как поля, показывает суть инкапсуляции действия — вся информация, необходимая и для выполнения (execute()), и для последующей отмены (undo()) этого конкретного действия, хранится непосредственно в самом объекте команды, что позволяет CommandHistory вызвать undo() для ЛЮБОЙ ранее выполненной команды значительно позже, без необходимости знать детали конкретного действия — достаточно общего интерфейса Command. Метод undo(), вызывающий deleteText (операцию, обратную выполненной в execute()), показывает ключевой принцип реализации отмены — каждая конкретная команда должна явно реализовать корректную, симметричную обратную операцию, и CommandHistory::undoLast() показывает простейшую реализацию истории как стека (алгоритмического цикла) — последняя выполненная команда отменяется первой (LIFO-порядок), что соответствует естественному, интуитивно ожидаемому поведению undo.
Подводные камни
- Реализация
undo(), не являющегося точной, корректной обратной операцией относительно соответствующегоexecute()— если обратная операция реализована неточно (например, удаляет неверное количество символов или с неверной позиции), последовательность execute/undo приведёт к документу, отличающемуся от исходного, до выполнения команды, состояния, что является серьёзной, подрывающей доверие пользователя ошибкой для функциональности отмены, и тщательное тестирование (основной цикл статей про тестирование) пары execute/undo для каждой конкретной команды особенно важно именно для этого паттерна. - Неограниченный рост истории команд без какого-либо предела, особенно для долгоработающих приложений с интенсивным использованием (например, текстовый редактор, открытый много часов с непрерывным редактированием) — каждая сохранённая команда занимает память, и без явного ограничения максимального размера истории (с удалением самых старых команд при превышении этого предела) история может неограниченно расти, потребляя всё больше памяти со временем без видимой пользователю причины.
- Сложность реализации
undo()для команд, чьё выполнение зависит от состояния, изменившегося между моментомexecute()и моментом последующегоundo()(например, удаление текста, который пользователь впоследствии отредактировал другими, не связанными командами в той же позиции) — для таких случаев простое, «наивное» хранение лишь параметров исходной операции может быть недостаточным для корректной отмены, и более надёжная реализация может потребовать сохранения дополнительного, более полного снимка релевантного состояния на момент выполнения (что концептуально пересекается с паттерном Memento, не входящим в данный курс статей, но решающим связанную задачу сохранения снимков состояния). - Игнорирование того, что некоторые действия принципиально необратимы или крайне сложны для корректной отмены (отправка сетевого запроса с реальными, внешними побочными эффектами, удаление файла с диска без сохранения резервной копии) — для таких действий попытка реализовать полноценный, надёжный
undo()может быть нецелесообразной или вовсе невозможной, и архитектура, основанная на Command, должна явно учитывать эту категорию необратимых действий, возможно, предупреждая пользователя о невозможности отмены перед реальным выполнением такого, принципиально необратимого действия.