C++ Command 模式与撤销/重做功能实现

哈喽,各位好!今天咱们来聊聊一个挺有意思的设计模式,叫Command模式。这玩意儿听起来高大上,但其实用起来很顺手,尤其是在需要实现撤销/重做功能的时候,简直就是神器。

Command模式是啥? 简单来说就是把一个请求或者操作封装成一个对象。

想象一下,你在玩一个游戏,你按了一下“跳跃”按钮。在Command模式的世界里,这个“跳跃”不是直接执行,而是被封装成一个“跳跃命令”的对象。这个对象知道谁要跳跃(接收者),以及怎么跳跃(执行方法)。 这样一来,我们就可以把这个命令对象存储起来,稍后执行,甚至撤销。

Command模式的组成部分

Command模式主要包含以下几个角色:

  • Command(命令): 这是一个接口或者抽象类,定义了执行命令的接口 execute()。 所有的具体命令类都要实现这个接口。
  • ConcreteCommand(具体命令): 这是实现了Command接口的具体类。它关联一个接收者对象,并调用接收者的相应方法来执行命令。
  • Receiver(接收者): 这是真正执行命令的对象。它知道如何完成请求所需的具体操作。
  • Invoker(调用者): 这是负责调用命令的对象。它不关心命令是如何执行的,只负责在合适的时机调用命令的 execute() 方法。
  • Client(客户端): 负责创建具体的命令对象,并设置其接收者。

举个例子:遥控器

我们用一个遥控器来理解一下。遥控器就是一个Invoker,每个按钮就是一个Command,电视机就是Receiver。你按下“开机”按钮,遥控器就调用了“开机命令”的 execute() 方法,然后“开机命令”就调用了电视机的开机功能。

Command模式的优点

  • 解耦: Invoker和Receiver之间完全解耦,Invoker不需要知道Receiver是谁,以及如何执行命令。
  • 可扩展性: 可以很容易地添加新的命令,而不需要修改现有的代码。
  • 支持撤销/重做: 因为命令被封装成对象,所以可以很方便地实现撤销和重做功能。
  • 支持队列化: 可以将多个命令放入队列中,然后依次执行。
  • 日志记录: 可以记录执行过的命令,方便调试和分析。

Command模式的缺点

  • 类数量增加: 每个命令都需要创建一个类,可能会导致类的数量增加。
  • 复杂性增加: 对于简单的操作,使用Command模式可能会增加代码的复杂性。

代码实现:一个简单的计算器

咱们用一个简单的计算器来演示Command模式。这个计算器支持加法、减法操作,并且可以撤销和重做。

#include <iostream>
#include <vector>

// Receiver: 计算器
class Calculator {
public:
    int currentValue = 0;

    void add(int operand) {
        currentValue += operand;
        std::cout << "加法: " << operand << ", 当前值: " << currentValue << std::endl;
    }

    void subtract(int operand) {
        currentValue -= operand;
        std::cout << "减法: " << operand << ", 当前值: " << currentValue << std::endl;
    }

    int getValue() const {
        return currentValue;
    }
};

// Command: 命令接口
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void unexecute() = 0; // 撤销操作
    virtual int getOperand() const { return 0; } // 用于记录操作数,方便撤销
};

// ConcreteCommand: 加法命令
class AddCommand : public Command {
private:
    Calculator* calculator;
    int operand;

public:
    AddCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}

    void execute() override {
        calculator->add(operand);
    }

    void unexecute() override {
        calculator->subtract(operand); // 撤销加法就是减法
    }

    int getOperand() const override {
        return operand;
    }
};

// ConcreteCommand: 减法命令
class SubtractCommand : public Command {
private:
    Calculator* calculator;
    int operand;

public:
    SubtractCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}

    void execute() override {
        calculator->subtract(operand);
    }

    void unexecute() override {
        calculator->add(operand); // 撤销减法就是加法
    }

     int getOperand() const override {
        return operand;
    }
};

// Invoker: 命令调用者
class CommandManager {
private:
    std::vector<Command*> history; // 命令历史记录
    int currentIndex = -1;        // 当前命令索引
    Calculator* calculator;

public:
    CommandManager(Calculator* calculator) : calculator(calculator) {}

    ~CommandManager() {
        for (Command* command : history) {
            delete command;
        }
    }

    void executeCommand(Command* command) {
        // 删除当前索引之后的所有命令,相当于清空redo栈
        while (history.size() > currentIndex + 1) {
            delete history.back();
            history.pop_back();
        }

        command->execute();
        history.push_back(command);
        currentIndex++;
    }

    void undo() {
        if (currentIndex >= 0) {
            history[currentIndex]->unexecute();
            currentIndex--;
        } else {
            std::cout << "无法撤销" << std::endl;
        }
    }

    void redo() {
        if (currentIndex < history.size() - 1) {
            currentIndex++;
            history[currentIndex]->execute();
        } else {
            std::cout << "无法重做" << std::endl;
        }
    }

    int getCurrentValue() const {
        return calculator->getValue();
    }
};

int main() {
    Calculator calculator;
    CommandManager commandManager(&calculator);

    // 执行加法命令
    commandManager.executeCommand(new AddCommand(&calculator, 5)); // 当前值: 5
    commandManager.executeCommand(new AddCommand(&calculator, 3)); // 当前值: 8
    commandManager.executeCommand(new SubtractCommand(&calculator, 2)); // 当前值: 6

    std::cout << "当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 6

    // 撤销操作
    commandManager.undo(); // 撤销减法, 当前值: 8
    std::cout << "撤销后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 8
    commandManager.undo(); // 撤销加法, 当前值: 5
    std::cout << "再次撤销后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 5

    // 重做操作
    commandManager.redo(); // 重做加法, 当前值: 8
    std::cout << "重做后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 8
    commandManager.redo(); // 重做减法, 当前值: 6
    std::cout << "再次重做后, 当前值: " << commandManager.getCurrentValue() << std::endl; // 输出: 6
    commandManager.redo(); // 无法重做
    commandManager.undo();
    commandManager.executeCommand(new AddCommand(&calculator, 10)); // 当前值: 16 之前的redo操作全部失效

    return 0;
}

代码解释

  1. Calculator (Receiver): 这是真正的计算器类,负责执行加法和减法操作。currentValue 存储当前的值。
  2. Command (Command): 这是一个抽象类,定义了 execute()unexecute() 方法。所有具体的命令类都要继承它。getOperand()用于获取操作数,方便撤销。
  3. AddCommand & SubtractCommand (ConcreteCommand): 这两个类分别实现了加法和减法命令。它们关联一个 Calculator 对象,并在 execute() 方法中调用 Calculator 相应的加法或减法方法。unexecute() 方法用于撤销操作,加法的撤销是减法,减法的撤销是加法。
  4. CommandManager (Invoker): 这个类负责管理命令的历史记录。executeCommand() 方法执行命令,并将命令添加到历史记录中。undo() 方法撤销上一个命令,redo() 方法重做下一个命令。history 是一个 std::vector<Command*>,用于存储命令的历史记录。currentIndex 记录当前命令的索引。

撤销/重做的实现

撤销/重做的关键在于 CommandManager 类中的 history 向量和 currentIndex 变量。

  • history: 存储了所有执行过的命令对象。
  • currentIndex: 指向当前可以撤销的命令的索引。

当执行一个新命令时,命令会被添加到 history 向量中,并且 currentIndex 会增加。当执行 undo() 操作时,currentIndex 会减少,并且 history[currentIndex] 对应的命令的 unexecute() 方法会被调用。当执行 redo() 操作时,currentIndex 会增加,并且 history[currentIndex] 对应的命令的 execute() 方法会被调用。

更复杂的情况:状态保存

上面的例子很简单,但是如果 Receiver 的状态很复杂,直接通过 unexecute() 来恢复状态可能会很困难。 比如,一个文本编辑器,如果每次撤销都要重新计算整个文本的排版,效率会很低。

这时候,可以考虑在 Command 对象中保存执行命令之前的 Receiver 的状态。这样,撤销的时候,直接把 Receiver 的状态恢复到之前保存的状态就可以了。

// 假设 Calculator 类更复杂,需要保存状态
class Calculator {
public:
    int currentValue = 0;
    // ... 其他状态 ...

    // 保存当前状态
    struct Memento {
        int currentValue;
        // ... 其他状态 ...
    };

    Memento saveState() {
        Memento m;
        m.currentValue = currentValue;
        // ... 保存其他状态 ...
        return m;
    }

    // 恢复到指定状态
    void restoreState(const Memento& m) {
        currentValue = m.currentValue;
        // ... 恢复其他状态 ...
    }

    void add(int operand) {
        currentValue += operand;
        std::cout << "加法: " << operand << ", 当前值: " << currentValue << std::endl;
    }

    void subtract(int operand) {
        currentValue -= operand;
        std::cout << "减法: " << operand << ", 当前值: " << currentValue << std::endl;
    }

    int getValue() const {
        return currentValue;
    }
};

// 修改后的 Command
class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
    virtual void unexecute() = 0;
};

class AddCommand : public Command {
private:
    Calculator* calculator;
    int operand;
    Calculator::Memento previousState; // 保存之前的状态

public:
    AddCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}

    void execute() override {
        previousState = calculator->saveState(); // 保存状态
        calculator->add(operand);
    }

    void unexecute() override {
        calculator->restoreState(previousState); // 恢复状态
    }
};

class SubtractCommand : public Command {
private:
    Calculator* calculator;
    int operand;
    Calculator::Memento previousState; // 保存之前的状态

public:
    SubtractCommand(Calculator* calculator, int operand) : calculator(calculator), operand(operand) {}

    void execute() override {
        previousState = calculator->saveState(); // 保存状态
        calculator->subtract(operand);
    }

    void unexecute() override {
        calculator->restoreState(previousState); // 恢复状态
    }
};

// CommandManager 不需要修改,因为 Command 已经负责状态管理

在这个例子中,Calculator 类增加了一个 Memento 结构体,用于保存和恢复状态。AddCommandSubtractCommand 类在 execute() 方法中保存了 Calculator 的状态,在 unexecute() 方法中恢复了状态。 这种方式更加通用,适用于复杂的 Receiver 对象。

Command模式的应用场景

  • GUI应用程序: 菜单项、按钮等操作都可以封装成命令,方便实现撤销/重做功能。
  • 事务处理: 数据库事务可以看作是一系列命令的集合,可以进行提交和回滚操作。
  • 游戏开发: 玩家的操作可以封装成命令,方便实现录像和回放功能。
  • 工作流引擎: 工作流中的每个步骤可以封装成命令,方便实现流程的控制和管理.
  • 日志系统: 记录操作日志,方便问题排查和审计。

总结

Command模式是一种非常实用的设计模式,它可以将请求封装成对象,从而实现解耦、可扩展性、撤销/重做等功能。虽然它可能会增加类的数量,但对于需要灵活控制和管理操作的场景来说,Command模式绝对是一个值得考虑的选择。

希望今天的讲解对大家有所帮助!记住,设计模式不是银弹,要根据实际情况选择合适的模式。 编码愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注