命令模式
本课属于 Coddy C++ 学习旅程的“面向对象编程”单元。第 96 课,共 104 课。
命令模式将请求封装为对象,让你可以用不同的请求对客户端进行参数化、将操作排队,或支持撤销功能。它将调用操作的对象与知道如何执行该操作的对象解耦。
这个模式有四个关键组成部分:包含 execute() 方法的 Command 接口、实现具体操作的 Concrete Commands、执行实际工作的 Receiver,以及触发命令的 Invoker:
#include <iostream>
#include <memory>
#include <vector>
// 接收者:知道如何执行操作
class Light {
public:
void turnOn() { std::cout << "Light is ON\n"; }
void turnOff() { std::cout << "Light is OFF\n"; }
};
// 命令接口
class Command {
public:
virtual void execute() = 0;
virtual void undo() = 0;
virtual ~Command() = default;
};
// 具体命令
class LightOnCommand : public Command {
Light& light;
public:
LightOnCommand(Light& l) : light(l) {}
void execute() override { light.turnOn(); }
void undo() override { light.turnOff(); }
};
class LightOffCommand : public Command {
Light& light;
public:
LightOffCommand(Light& l) : light(l) {}
void execute() override { light.turnOff(); }
void undo() override { light.turnOn(); }
};
// 调用者
class RemoteControl {
std::vector<std::unique_ptr<Command>> history;
public:
void pressButton(std::unique_ptr<Command> cmd) {
cmd->execute();
history.push_back(std::move(cmd));
}
void pressUndo() {
if (!history.empty()) {
history.back()->undo();
history.pop_back();
}
}
};每个命令都会保存对其接收者的引用,并在执行时调用相应的方法。调用者不知道会执行什么操作,它只会调用 execute()。通过将命令保存在历史记录中,你可以轻松实现撤销功能,只需调用每个命令的 undo() 方法。
当你需要对操作进行排队、实现撤销/重做,或将请求发送方与处理程序解耦时,使用 Command。
挑战
简单让我们使用命令模式,构建一个支持撤销功能的文本编辑器。你将创建一个系统,把编辑操作(例如插入和删除文本)封装成命令对象,让用户可以执行操作,然后按相反顺序撤销,就像真正的文本编辑器一样。
你将把代码分到四个文件中:
TextDocument.h:创建接收者类,用于保存和操作实际的文本内容。你的
TextDocument类应将文档内容存储为字符串。请实现以下方法:insertText(const std::string& text):将文本追加到内容末尾deleteText(int count):从内容中删除最后count个字符(如果 count 超出内容长度,则不执行任何操作)getContent():以 const 引用的形式返回当前内容
Command.h:定义命令接口和具体命令类。创建一个抽象
Command类,包含纯虚方法execute()和undo(),以及虚析构函数。然后实现两个具体命令:
InsertCommand:接收对TextDocument的引用和要插入的字符串。执行时插入文本。撤销时删除插入的相同数量的字符。DeleteCommand:接收对TextDocument的引用和要删除的字符数量。它需要保存删除的文本,以便撤销时恢复。执行时从末尾删除字符。撤销时重新插入保存的文本。
TextEditor.h:创建调用者,负责管理命令的执行和历史记录。你的
TextEditor类应持有对TextDocument的引用,并使用由unique_ptr<Command>组成的 vector 保存已执行命令的历史记录。请实现:executeCommand(std::unique_ptr<Command> cmd):执行命令并将其添加到历史记录中undo():撤销最近执行的命令,并将其从历史记录中移除(如果历史记录为空,则不执行任何操作)showContent():输出当前文档内容;如果内容为空,则输出[empty]
main.cpp:演示带有撤销功能的命令模式。读取三个输入:
- 要插入的第一段文本(字符串)
- 要插入的第二段文本(字符串)
- 要删除的字符数量(整数)
创建一个
TextDocument和一个TextEditor。然后按顺序执行以下操作,并在每一步后显示内容:- 插入第一段文本,然后显示内容
- 插入第二段文本,然后显示内容
- 删除指定数量的字符,然后显示内容
- 撤销一次,然后显示内容
- 再撤销一次,然后显示内容
例如,输入 Hello、 World 和 3 时:
Hello
Hello World
Hello Wo
Hello World
Hello输入 Code、Editor 和 6 时:
Code
CodeEditor
Code
CodeEditor
Code注意,每个命令都知道如何撤销自身。DeleteCommand 会记住它删除的内容,以便恢复;InsertCommand 则准确知道撤销时要删除多少个字符。这就是命令模式的强大之处:操作变成了一等对象,可以被存储、执行和撤销。
亲手试试
#include <iostream>
#include <string>
#include <memory>
#include "TextDocument.h"
#include "Command.h"
#include "TextEditor.h"
int main() {
// 读取输入
std::string firstText;
std::string secondText;
int deleteCount;
std::getline(std::cin, firstText);
std::getline(std::cin, secondText);
std::cin >> deleteCount;
// TODO:创建一个 TextDocument 和一个 TextEditor
// TODO:按顺序执行操作,每次操作后显示内容:
// 1. 插入第一段文本,然后显示内容
// 2. 插入第二段文本,然后显示内容
// 3. 删除指定数量的字符,然后显示内容
// 4. 撤销一次,然后显示内容
// 5. 再撤销一次,然后显示内容
// 提示:使用 std::make_unique<InsertCommand>(...) 和
// std::make_unique<DeleteCommand>(...) 创建命令
return 0;
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C++ 在线编译器