命令模式
命令模式是一种行为设计模式,它将请求封装成一个对象,从而使我们可以将不同的请求、队列或日志请求等参数化,同时支持可撤销的操作。该模式的核心思想是将请求发送者和接收者解耦,让它们不直接交互,而是通过命令对象进行交互,即将请求封装成类对象。命令模式通常用于以下场景:
需要将请求发送者和接收者解耦的场景,以便于适应变化。
需要支持撤销和恢复操作的场景。
需要将一组操作组合在一起执行的场景,也称为批处理。
命令模式包含以下角色:
命令(Command):定义命令的接口,通常包含执行和撤销两个方法。
具体命令(ConcreteCommand):实现命令接口,包含了对应的操作。
命令接收者(Receiver):执行命令的对象。
命令发起者(Invoker):调用命令的对象,负责将命令发送给命令接收者。
客户端(Client):创建命令对象并将其发送给命令发起者。
下面给出一个命令模式的C++示例:
class Command {
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0;
};
class ConcreteCommand : public Command {
public:
ConcreteCommand(std::shared_ptr<Receiver> receiver) : m_receiver(receiver) {}
virtual void execute() {
m_receiver->action();
}
virtual void undo() {
m_receiver->undoAction();
}
private:
std::shared_ptr<Receiver> m_receiver;
};
class Receiver {
public:
void action() {
// 执行操作
}
void undoAction() {
// 撤销操作
}
};
class Invoker {
public:
void setCommand(std::shared_ptr<Command> command) {
m_command = command;
}
void executeCommand() {
m_command->execute();
}
void undoCommand() {
m_command->undo();
}
private:
std::shared_ptr<Command> m_command;
};
int main() {
auto receiver = std::make_shared<Receiver>();
auto command = std::make_shared<ConcreteCommand>(receiver);
auto invoker = std::make_shared<Invoker>();
invoker->setCommand(command);
invoker->executeCommand();
invoker->undoCommand();
return 0;
}
在上面的示例中,Command类是命令接口,定义了execute和undo方法。ConcreteCommand类是具体命令类,实现了Command接口,包含了对应的操作。Receiver类是命令接收者,执行命令的对象。Invoker类是命令发起者,调用命令的对象,负责将命令发送给命令接收者。在客户端代码中,我们创建了一个Receiver对象和一个ConcreteCommand对象,并将其传递给Invoker对象。然后,我们调用Invoker对象的executeCommand方法来执行ConcreteCommand对象的操作。如果需要撤销操作,我们可以调用Invoker对象的undoCommand方法来执行ConcreteCommand对象的undo操作。
撤销/重做框架(QUndoStack、QUndoCommand等类)
Qt的撤销/重做框架(QUndoStack
、QUndoCommand
等类)是命令模式的一种实现。在Qt的撤销/重做框架中,每个操作都被封装为一个QUndoCommand
的子类对象。
以下是一个使用Qt的撤销/重做框架的程序示例:
#include <QApplication>
#include <QTextEdit>
#include <QUndoStack>
#include <QPushButton>
#include <QVBoxLayout>
#include <QUndoCommand>
class MyCommand : public QUndoCommand
{
public:
MyCommand(QTextEdit *editor, const QString &text, QUndoCommand *parent = nullptr)
: QUndoCommand(parent), m_editor(editor), m_text(text), m_oldText(editor->toPlainText()) {}
void undo() override
{
m_editor->setPlainText(m_oldText);
}
void redo() override
{
m_editor->setPlainText(m_text);
}
private:
QTextEdit *m_editor;
QString m_text;
QString m_oldText;
};
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QUndoStack stack;
QTextEdit editor;
QPushButton undoButton("Undo");
QPushButton redoButton("Redo");
QObject::connect(&undoButton, &QPushButton::clicked, &stack, &QUndoStack::undo);
QObject::connect(&redoButton, &QPushButton::clicked, &stack, &QUndoStack::redo);
QObject::connect(&editor, &QTextEdit::textChanged, [&]() {
stack.push(new MyCommand(&editor, editor.toPlainText()));
});
QVBoxLayout layout;
layout.addWidget(&editor);
layout.addWidget(&undoButton);
layout.addWidget(&redoButton);
QWidget window;
window.setLayout(&layout);
window.show();
return app.exec();
}
在上述示例中,MyCommand
是 QUndoCommand
的子类。每次 QTextEdit
的文本改变时,就会创建一个新的 MyCommand
对象并将其压入 QUndoStack
。当点击 "Undo" 按钮时,就会撤销栈顶的命令;当点击 "Redo" 按钮时,就会重做栈顶的命令。
在这个例子中,相关的类扮演的角色如下:
命令(Command):
QUndoCommand
是命令接口。它定义了执行(redo)和撤销(undo)的方法。具体命令(ConcreteCommand):
MyCommand
是具体的命令。它是QUndoCommand
的子类,实现了redo
和undo
方法。命令接收者(Receiver):
QTextEdit
是命令的接收者。它是执行命令的对象,MyCommand
的redo
和undo
方法都是操作QTextEdit
。命令发起者(Invoker):
QUndoStack
是命令的发起者。它负责调用和存储命令。QPushButton
实际上也扮演了命令发起者的角色,因为它们是触发执行或撤销命令的实际用户界面元素。客户端(Client):在这个例子中,main 函数就是客户端。它创建了应用程序,包括命令接收者(
QTextEdit
)、命令发起者(QUndoStack
和QPushButton
)、并在QTextEdit
的文本变化时创建具体命令(MyCommand
)。
下面给出相关的类在Qt源码中的实现。同样,这里隐去了很多与命令模式无关的细节,但对理解命令模式应该是足够的。
class QUndoCommand
{
public:
virtual ~QUndoCommand() {}
virtual void undo() = 0;
virtual void redo() = 0;
};
class QUndoStack
{
public:
void push(QUndoCommand *cmd)
{
m_stack.push(cmd);
cmd->redo();
}
void undo()
{
if (!m_stack.isEmpty()) {
QUndoCommand *cmd = m_stack.pop();
cmd->undo();
m_undoStack.push(cmd);
}
}
void redo()
{
if (!m_undoStack.isEmpty()) {
QUndoCommand *cmd = m_undoStack.pop();
cmd->redo();
m_stack.push(cmd);
}
}
private:
QStack<QUndoCommand*> m_stack;
QStack<QUndoCommand*> m_undoStack;
};
MyCommand
类我们已经在前面实现了,这里不再重复。对于命令的接收者QTextEdit
,我们也不需要关注它的源码,因为它的功能就是一个文本编辑器,我们只需要知道它提供了setPlainText()
和toPlainText()
等函数供我们在MyCommand
中使用就可以了。
需要注意的是,在标准的命令模式中,通常只有一个存储命令对象的容器,可以是是队列或栈,也可以仅仅是只是单个命令对象(如我们一开始给出的命令模式的示例)。然而,在实现撤销/重做功能时,通常需要两个栈结构。
在Qt的QUndoStack
中,主栈用于存储执行过的命令,当调用undo()
方法时,会从主栈中弹出命令并执行其undo操作,同时该命令会被压入撤销栈。撤销栈用于存储撤销过的命令,当调用redo()
方法时,会从撤销栈中弹出命令并执行其redo操作,同时该命令会被压回主栈。这样的设计使得QUndoStack
能够按正确的顺序执行和撤销命令,同时还能在撤销命令后重新执行它们。
总结
Qt的撤销/重做框架,实现了命令模式,并使用了两个栈的方式维护了操作的历史记录,确实是很精妙的设计。除此之外,Qt的撤销/重做框架是支持多个命令的合并的,这在文字编辑或者其他需要撤销/重做框架的需求中,都是很有用的。QUndoCommand
类提供了一个可重写的mergeWith
方法,可以用来合并连续的、类似的操作,使其在撤销/重做时被视为一个单一的操作。这里由于篇幅问题,不展开讨论。总的来说,Qt的撤销/重做框架,很好地实现了命令模式。