Menu
CoddyTech

命令模式

本课属于 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。

challenge icon

挑战

简单

让我们使用命令模式,构建一个支持撤销功能的文本编辑器。你将创建一个系统,把编辑操作(例如插入和删除文本)封装成命令对象,让用户可以执行操作,然后按相反顺序撤销,就像真正的文本编辑器一样。

你将把代码分到四个文件中:

  • 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:演示带有撤销功能的命令模式。

    读取三个输入:

    1. 要插入的第一段文本(字符串)
    2. 要插入的第二段文本(字符串)
    3. 要删除的字符数量(整数)

    创建一个 TextDocument 和一个 TextEditor。然后按顺序执行以下操作,并在每一步后显示内容:

    1. 插入第一段文本,然后显示内容
    2. 插入第二段文本,然后显示内容
    3. 删除指定数量的字符,然后显示内容
    4. 撤销一次,然后显示内容
    5. 再撤销一次,然后显示内容

例如,输入 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;
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C++ 在线编译器