命令模式
命令模式是一種行為設(shè)計(jì)模式,它將請(qǐng)求封裝成一個(gè)對(duì)象,從而使我們可以將不同的請(qǐng)求、隊(duì)列或日志請(qǐng)求等參數(shù)化,同時(shí)支持可撤銷的操作。該模式的核心思想是將請(qǐng)求發(fā)送者和接收者解耦,讓它們不直接交互,而是通過命令對(duì)象進(jìn)行交互,即將請(qǐng)求封裝成類對(duì)象。命令模式通常用于以下場(chǎng)景:
需要將請(qǐng)求發(fā)送者和接收者解耦的場(chǎng)景,以便于適應(yīng)變化。
需要支持撤銷和恢復(fù)操作的場(chǎng)景。
需要將一組操作組合在一起執(zhí)行的場(chǎng)景,也稱為批處理。
命令模式包含以下角色:
命令(Command):定義命令的接口,通常包含執(zhí)行和撤銷兩個(gè)方法。
具體命令(ConcreteCommand):實(shí)現(xiàn)命令接口,包含了對(duì)應(yīng)的操作。
命令接收者(Receiver):執(zhí)行命令的對(duì)象。
命令發(fā)起者(Invoker):調(diào)用命令的對(duì)象,負(fù)責(zé)將命令發(fā)送給命令接收者。
客戶端(Client):創(chuàng)建命令對(duì)象并將其發(fā)送給命令發(fā)起者。

下面給出一個(gè)命令模式的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() {
// 執(zhí)行操作
}
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類是具體命令類,實(shí)現(xiàn)了Command接口,包含了對(duì)應(yīng)的操作。Receiver類是命令接收者,執(zhí)行命令的對(duì)象。Invoker類是命令發(fā)起者,調(diào)用命令的對(duì)象,負(fù)責(zé)將命令發(fā)送給命令接收者。在客戶端代碼中,我們創(chuàng)建了一個(gè)Receiver對(duì)象和一個(gè)ConcreteCommand對(duì)象,并將其傳遞給Invoker對(duì)象。然后,我們調(diào)用Invoker對(duì)象的executeCommand方法來執(zhí)行ConcreteCommand對(duì)象的操作。如果需要撤銷操作,我們可以調(diào)用Invoker對(duì)象的undoCommand方法來執(zhí)行ConcreteCommand對(duì)象的undo操作。
撤銷/重做框架(QUndoStack、QUndoCommand等類)
Qt的撤銷/重做框架(QUndoStack、QUndoCommand等類)是命令模式的一種實(shí)現(xiàn)。在Qt的撤銷/重做框架中,每個(gè)操作都被封裝為一個(gè)QUndoCommand的子類對(duì)象。
以下是一個(gè)使用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 的文本改變時(shí),就會(huì)創(chuàng)建一個(gè)新的 MyCommand 對(duì)象并將其壓入 QUndoStack。當(dāng)點(diǎn)擊 "Undo" 按鈕時(shí),就會(huì)撤銷棧頂?shù)拿?;?dāng)點(diǎn)擊 "Redo" 按鈕時(shí),就會(huì)重做棧頂?shù)拿睢?/p>
在這個(gè)例子中,相關(guān)的類扮演的角色如下:
命令(Command):
QUndoCommand是命令接口。它定義了執(zhí)行(redo)和撤銷(undo)的方法。具體命令(ConcreteCommand):
MyCommand是具體的命令。它是QUndoCommand的子類,實(shí)現(xiàn)了redo和undo方法。命令接收者(Receiver):
QTextEdit是命令的接收者。它是執(zhí)行命令的對(duì)象,MyCommand的redo和undo方法都是操作QTextEdit。命令發(fā)起者(Invoker):
QUndoStack是命令的發(fā)起者。它負(fù)責(zé)調(diào)用和存儲(chǔ)命令。QPushButton實(shí)際上也扮演了命令發(fā)起者的角色,因?yàn)樗鼈兪怯|發(fā)執(zhí)行或撤銷命令的實(shí)際用戶界面元素。客戶端(Client):在這個(gè)例子中,main 函數(shù)就是客戶端。它創(chuàng)建了應(yīng)用程序,包括命令接收者(
QTextEdit)、命令發(fā)起者(QUndoStack和QPushButton)、并在QTextEdit的文本變化時(shí)創(chuàng)建具體命令(MyCommand)。
下面給出相關(guān)的類在Qt源碼中的實(shí)現(xiàn)。同樣,這里隱去了很多與命令模式無關(guān)的細(xì)節(jié),但對(duì)理解命令模式應(yīng)該是足夠的。
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類我們已經(jīng)在前面實(shí)現(xiàn)了,這里不再重復(fù)。對(duì)于命令的接收者QTextEdit,我們也不需要關(guān)注它的源碼,因?yàn)樗墓δ芫褪且粋€(gè)文本編輯器,我們只需要知道它提供了setPlainText()和toPlainText()等函數(shù)供我們?cè)?code>MyCommand中使用就可以了。
需要注意的是,在標(biāo)準(zhǔn)的命令模式中,通常只有一個(gè)存儲(chǔ)命令對(duì)象的容器,可以是是隊(duì)列或棧,也可以僅僅是只是單個(gè)命令對(duì)象(如我們一開始給出的命令模式的示例)。然而,在實(shí)現(xiàn)撤銷/重做功能時(shí),通常需要兩個(gè)棧結(jié)構(gòu)。
在Qt的QUndoStack中,主棧用于存儲(chǔ)執(zhí)行過的命令,當(dāng)調(diào)用undo()方法時(shí),會(huì)從主棧中彈出命令并執(zhí)行其undo操作,同時(shí)該命令會(huì)被壓入撤銷棧。撤銷棧用于存儲(chǔ)撤銷過的命令,當(dāng)調(diào)用redo()方法時(shí),會(huì)從撤銷棧中彈出命令并執(zhí)行其redo操作,同時(shí)該命令會(huì)被壓回主棧。這樣的設(shè)計(jì)使得QUndoStack能夠按正確的順序執(zhí)行和撤銷命令,同時(shí)還能在撤銷命令后重新執(zhí)行它們。
總結(jié)
Qt的撤銷/重做框架,實(shí)現(xiàn)了命令模式,并使用了兩個(gè)棧的方式維護(hù)了操作的歷史記錄,確實(shí)是很精妙的設(shè)計(jì)。除此之外,Qt的撤銷/重做框架是支持多個(gè)命令的合并的,這在文字編輯或者其他需要撤銷/重做框架的需求中,都是很有用的。QUndoCommand類提供了一個(gè)可重寫的mergeWith方法,可以用來合并連續(xù)的、類似的操作,使其在撤銷/重做時(shí)被視為一個(gè)單一的操作。這里由于篇幅問題,不展開討論??偟膩碚f,Qt的撤銷/重做框架,很好地實(shí)現(xiàn)了命令模式。