命令模式
命令模式是一種行為設(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è)單一的操作捷雕。這里由于篇幅問題椒丧,不展開討論【认铮總的來說壶熏,Qt的撤銷/重做框架,很好地實(shí)現(xiàn)了命令模式浦译。