命令模式(Command Pattern):把"一个请求 / 一次操作"封装成一个对象——这个对象携带了做什么、对谁做、需要哪些参数的全部信息,从而让"发起请求的人"(按钮、菜单、快捷键)与"真正干活的人"(业务对象)彻底解耦。一句话:把操作变成一张可以传递、排队、记录、撤销的"纸条"。
这是行为型模式中最"实用主义"的一个:它不改变对象之间的数据结构,而是改变请求的传递方式——从"直接调用"变成"封装成对象再调用"。正因为请求成了对象,撤销、重做、宏录制、操作日志、事务回滚这些高级能力才成为可能。Qt 中的 QUndoStack / QUndoCommand、QAction 都是命令模式的标准示范。
先看一段没有命令模式的"坏代码"。假设你正在写一个文本编辑器,窗口里有一个"追加文本"按钮,最直接的写法是让按钮直接调用业务对象的方法:
// 坏味道:界面层直接调用业务层,撤销、复用、测试全都无从谈起
#include <string>
#include <iostream>
class TextEditor { // 接收者:真正干活的对象
public:
void appendText(const std::string& text) { content_ += text; }
std::string content() const { return content_; }
private:
std::string content_;
};
class EditorWindow { // 界面:既当 Invoker 又当 Client
public:
void onAppendClicked(const std::string& text) {
editor_.appendText(text); // 界面与业务硬耦合
std::cout << "当前内容: " << editor_.content() << "\n";
}
private:
TextEditor editor_; // 窗口直接持有并依赖具体业务类
};
这段代码的问题:
① 违反 SRP(单一职责):按钮事件函数同时承担"捕获用户操作""决定调用谁""刷新界面"三类职责;
② 违反 OCP(开闭原则):每新增一个操作(插入图片、加粗、删除…),就要在 EditorWindow 里加一个方法;而菜单、工具栏按钮、快捷键三个入口各自都要写一遍调用代码——改一个业务接口,所有入口跟着改;
③ 无法撤销 / 重做:调用即执行,没有中间对象记录"刚才做了什么"。想做 Ctrl+Z,只能退回到保存整份文本快照的笨办法,又慢又容易出错;
④ 无法排队、录制宏、写操作日志:操作不是对象,就不能被存进队列延迟执行、不能被录制回放、不能被序列化审计;
⑤ 测试困难:想测"点击追加"的逻辑,必须实例化整个窗口和真实业务对象,无法用假对象替换。
命令模式的解法:把"追加文本"这个操作本身封装成一个命令对象,窗口只负责"把命令交出去",命令内部才知道该调用哪个业务对象的哪个方法。于是——按钮、菜单、快捷键共享同一个命令;命令可以被压进历史栈实现撤销;命令可以被排队、被录制、被记录。
命令模式的核心是"请求对象化":把一次函数调用(editor.appendText("hello"))包装成一个对象(AppendCommand(editor, "hello"))。函数调用一旦变成对象,就获得了对象的一切特权——可以被存储、传递、组合、排队、序列化、延迟执行。
四个角色的分工:接收者(Receiver)负责真正的业务逻辑;命令(Command)负责绑定"接收者 + 参数 + 动作",只暴露统一的 execute() / undo() 接口;调用者(Invoker)只认识命令接口,按下按钮就调用 execute(),完全不关心背后是谁在干活;客户端(Client)负责组装——创建命令、指定接收者、把命令交给调用者。
生活类比:
· 餐厅点餐:顾客(Client)把订单(Command)交给服务员(Invoker),服务员不关心菜怎么做,只把订单传给厨师(Receiver)。订单本身可以被排队、被修改、被取消——"取消订单"就是撤销(undo)。如果顾客说"刚才那个菜不要了",服务员只需要找到那张订单。
· 遥控器:你按"音量+"(Invoker 触发),遥控器内部把按键翻译成一个红外指令包(Command)发给电视(Receiver)。换一台电视(换 Receiver),遥控器不用换——只要指令协议相同。
· 录音机的"宏":把"开机 → 选台 → 调音量 → 播放"录成一段宏命令,之后一键回放。宏就是命令的组合。
注意:命令模式与函数指针 / lambda 一脉相承——std::function + lambda 就是"轻量命令"。当需求只是"延迟执行",lambda 就够了;当需要撤销、重做、命令合并、命令组合(宏)时,才需要完整的命令类。
┌─────────────────────────────────────────────────────────────┐
│ Client(客户端) │
│ 创建 ConcreteCommand 并设置 Receiver,交给 Invoker │
└───────────────┬───────────────────────────────┬─────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Invoker │ │ Command(抽象) │
│ (调用者/遥控器) │───────▶│ + execute() │
│ 持有 Command 引用 │ 触发 │ + undo() │
│ 不知道 Receiver │ └──────────┬───────────┘
└──────────────────────┘ ▲
┌─────────┴──────────┐
│ ConcreteCommand │
│ (开灯命令/追加命令) │
│ 持有 receiver 引用 │
│ execute()→receiver.xxx│
└─────────┬──────────┘
│ 调用
▼
┌──────────────────────────┐
│ Receiver(接收者) │
│ 真正干活的业务对象 │
│ (Light / TextEditor) │
└──────────────────────────┘
角色职责:
· Command(抽象命令):声明 execute() 与 undo() 接口。这是整个模式的关键抽象——调用者只依赖它;
· ConcreteCommand(具体命令):持有接收者的引用和必要参数,在 execute() 里调用接收者的具体方法,在 undo() 里执行逆操作;
· Receiver(接收者):业务逻辑的归属者,对命令一无所知(命令反过来依赖它);
· Invoker(调用者):持有命令,在某个时机(按钮点击、快捷键、定时器)调用 execute()。它不知道命令内部做了什么;
· Client(客户端):组装一切——创建命令、绑定接收者、把命令交给调用者。它是唯一"知道所有细节"的角色。
用经典的"遥控器控制电灯"演示:电灯是接收者,开灯/关灯是命令,遥控器是调用者,并且支持 撤销(undo)。完整可编译(C++17),中文注释:
// command_demo.cpp —— 命令模式最小示例(C++17)
// 编译:g++ -std=c++17 -Wall -Wextra command_demo.cpp -o command_demo
#include <iostream>
#include <memory> // std::shared_ptr / std::make_shared
#include <string>
// ---------- Receiver:真正干活的电灯 ----------
class Light {
public:
void on() { std::cout << "💡 灯亮了\n"; isOn_ = true; }
void off() { std::cout << "🌑 灯灭了\n"; isOn_ = false; }
bool isOn() const { return isOn_; }
private:
bool isOn_ = false;
};
// ---------- Command:抽象命令接口 ----------
// 所有命令统一暴露 execute() / undo(),调用者只认识这两个方法
class Command {
public:
virtual ~Command() = default;
virtual void execute() = 0; // 执行
virtual void undo() = 0; // 撤销(逆操作)
};
// ---------- ConcreteCommand:开灯命令 ----------
class LightOnCommand : public Command {
public:
explicit LightOnCommand(Light& light) : light_(light) {}
void execute() override { light_.on(); } // 正向:开灯
void undo() override { light_.off(); } // 逆向:关灯
private:
Light& light_; // 引用接收者(不拥有它,避免所有权混乱)
};
// ---------- ConcreteCommand:关灯命令 ----------
class LightOffCommand : public Command {
public:
explicit LightOffCommand(Light& light) : light_(light) {}
void execute() override { light_.off(); } // 正向:关灯
void undo() override { light_.on(); } // 逆向:开灯
private:
Light& light_;
};
// ---------- Invoker:遥控器(不知道任何命令细节) ----------
class RemoteControl {
public:
void setCommand(std::shared_ptr<Command> cmd) { command_ = std::move(cmd); }
void pressButton() { if (command_) command_->execute(); }
void pressUndo() { if (command_) command_->undo(); }
private:
std::shared_ptr<Command> command_; // 只依赖抽象接口
};
// ---------- Client:组装一切 ----------
int main() {
Light livingRoomLight; // 接收者
auto lightOn = std::make_shared<LightOnCommand>(livingRoomLight);
auto lightOff = std::make_shared<LightOffCommand>(livingRoomLight);
RemoteControl remote; // 调用者
remote.setCommand(lightOn);
remote.pressButton(); // 输出:💡 灯亮了
remote.pressUndo(); // 输出:🌑 灯灭了
remote.setCommand(lightOff);
remote.pressButton(); // 输出:🌑 灯灭了
remote.pressUndo(); // 输出:💡 灯亮了
return 0;
}
程序输出:
💡 灯亮了
🌑 灯灭了
🌑 灯灭了
💡 灯亮了
注意几个细节:命令用引用持有接收者(命令不拥有接收者,生命周期由外部保证);遥控器只持有 std::shared_ptr<Command> 抽象指针,换一个命令对象就换一种行为——开灯/关灯的逻辑与按键完全解耦。若把命令换成 std::function<void()> + lambda,也可以写出等价的轻量版本,但就丢失了 undo() 这种"附带行为"的天然载体。
命令模式在 Qt 中不是"需要自己造的轮子"——Qt 官方提供了完整的命令模式框架:QUndoCommand + QUndoStack(撤销框架)和 QAction(动作封装)。下面用 Qt 6 写一个真实可运行的"文本追加器":按钮 / 快捷键都通过命令操作文档,Ctrl+Z 撤销、Ctrl+Y 重做由 QUndoStack 免费提供。
① 接收者——文本文档(TextDocument.h / TextDocument.cpp)
// TextDocument.h —— Receiver:真正存储文本的业务对象
#pragma once
#include <QObject>
#include <QString>
class TextDocument : public QObject {
Q_OBJECT
public:
explicit TextDocument(QObject* parent = nullptr);
void appendText(const QString& text); // 追加文本
void removeLast(int length); // 删除末尾 length 个字符
QString text() const { return text_; }
signals:
void textChanged(const QString& text); // 内容变化通知界面刷新
private:
QString text_;
};
// TextDocument.cpp
#include "TextDocument.h"
TextDocument::TextDocument(QObject* parent) : QObject(parent) {}
void TextDocument::appendText(const QString& text) {
text_ += text;
emit textChanged(text_);
}
void TextDocument::removeLast(int length) {
text_.chop(length); // 从末尾删除 length 个字符
emit textChanged(text_);
}
② 具体命令——追加命令(AppendCommand.h / AppendCommand.cpp)
// AppendCommand.h —— ConcreteCommand:一次"追加文本"操作,自带撤销
#pragma once
#include <QUndoCommand>
class TextDocument;
class AppendCommand : public QUndoCommand {
public:
AppendCommand(TextDocument* doc, const QString& text,
QUndoCommand* parent = nullptr);
void undo() override;
void redo() override;
private:
TextDocument* doc_; // 指向接收者(由外部保证生命周期)
QString text_; // 命令参数
};
// AppendCommand.cpp
#include "AppendCommand.h"
#include "TextDocument.h"
AppendCommand::AppendCommand(TextDocument* doc, const QString& text,
QUndoCommand* parent)
: QUndoCommand(parent), doc_(doc), text_(text) {
// setText 的内容会显示在"编辑→撤销"菜单里(用户可读描述)
setText(QStringLiteral("追加 \"%1\"").arg(text));
}
// 撤销 = 删掉刚追加的字符(逆操作)
void AppendCommand::undo() { doc_->removeLast(text_.size()); }
// 执行 = 追加文本(redo 在首次 push 时也会被调用)
void AppendCommand::redo() { doc_->appendText(text_); }
③ 组装——main.cpp(Invoker + Client 都在这里)
// main.cpp —— 按钮触发命令、QUndoStack 管理历史、Ctrl+Z/Ctrl+Y 撤销重做
#include <QApplication>
#include <QMainWindow>
#include <QWidget>
#include <QVBoxLayout>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QAction>
#include <QKeySequence>
#include <QUndoStack>
#include "TextDocument.h"
#include "AppendCommand.h"
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
TextDocument doc; // 接收者:真正的文档数据
QUndoStack undoStack; // 撤销栈:Invoker + 历史记录 + 命令所有权
QMainWindow window;
QWidget central;
QVBoxLayout layout(¢ral);
QTextEdit editor; // 只读展示区
editor.setReadOnly(true);
QLineEdit input; // 输入框
QPushButton appendButton(QStringLiteral("追加"));
QPushButton undoButton(QStringLiteral("撤销"));
layout.addWidget(&editor);
layout.addWidget(&input);
layout.addWidget(&appendButton);
layout.addWidget(&undoButton);
// 点击"追加":创建命令 → 压入撤销栈(push 会自动调用 redo() 执行)
QObject::connect(&appendButton, &QPushButton::clicked, [&] {
if (input.text().isEmpty()) return;
auto* cmd = new AppendCommand(&doc, input.text(), nullptr);
undoStack.push(cmd); // QUndoStack 接管命令所有权,负责 delete
input.clear();
});
QObject::connect(&undoButton, &QPushButton::clicked,
&undoStack, &QUndoStack::undo);
// 文档变化 → 刷新界面
QObject::connect(&doc, &TextDocument::textChanged,
&editor, &QTextEdit::setPlainText);
// 用 createUndoAction 一键生成"编辑→撤销"动作,自动带 Ctrl+Z 快捷键
QAction* undoAction = undoStack.createUndoAction(&window, QStringLiteral("&撤销"));
undoAction->setShortcut(QKeySequence::Undo); // Ctrl+Z
window.addAction(undoAction);
QAction* redoAction = undoStack.createRedoAction(&window, QStringLiteral("&重做"));
redoAction->setShortcut(QKeySequence::Redo); // Ctrl+Y
window.addAction(redoAction);
window.setCentralWidget(¢ral);
window.resize(420, 320);
window.show();
return app.exec();
}
④ CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(UndoDemo VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON) # 自动处理 Q_OBJECT 的 moc
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(UndoDemo
main.cpp
TextDocument.h TextDocument.cpp
AppendCommand.h AppendCommand.cpp
)
target_link_libraries(UndoDemo PRIVATE Qt6::Widgets)
这个例子展示了命令模式在 Qt 中的完整形态:
· QUndoStack::push() 会自动调用命令的 redo() 执行,并把命令压入撤销栈——Invoker + 历史记录二合一;
· undo() / redo() 由每个命令自己实现逆操作,栈只管"弹出并调用",完全不理解业务;
· 撤销栈默认容量 100 步,超限自动丢弃并 delete 最老的命令——内存安全由框架保证;
· 同一份文档可以被按钮、菜单、快捷键、脚本四处触发同一个命令对象——触发源与业务解耦;
· 若给命令实现 id() 和 mergeWith(),还能把连续的同类型操作合并为一步(比如连续输入合并成一次撤销)。
以 Qt 示例为主线,一次"追加 + 撤销"的完整旅程:
① 用户点击"追加"按钮:QPushButton::clicked 信号触发 lambda;lambda 检查输入框非空后,创建 AppendCommand 对象(内部记录接收者 &doc 和参数 input.text()),并调用 undoStack.push(cmd);
② 压栈并执行:QUndoStack::push() 把命令加入撤销栈(成为栈顶),随后调用 cmd->redo()——命令在这里才真正"执行";
③ 接收者干活:redo() 调用 doc->appendText(text_),TextDocument 修改内部 QString 并发射 textChanged 信号;
④ 界面刷新:信号连接到 QTextEdit::setPlainText,文本框显示新内容——界面只订阅信号,不参与任何业务逻辑;
⑤ 用户按 Ctrl+Z:undoAction(由 createUndoAction 生成)触发 undoStack.undo();
⑥ 弹栈撤销:undo() 从栈顶取出命令并调用其 undo()——doc->removeLast(text_.size()) 删掉刚追加的字符,再次发射信号刷新界面;被弹出的命令进入"重做栈",按 Ctrl+Y 可原路重放;
⑦ 生命周期收尾:继续撤销直到重做栈为空、或新命令压栈清空重做栈;历史超过 100 步时,最老的命令被 delete——整个过程中 main() 只负责组装,业务代码一行都没有。
对比最小示例(遥控器):第 ①② 步对应 setCommand + pressButton,第 ⑤⑥ 步对应 pressUndo——结构完全一致,只是 Qt 版把"历史栈"这个调用者升级成了完整的撤销框架。
解耦点:调用者与接收者。Invoker 只依赖抽象的 Command::execute(),不知道、也不需要知道命令背后是谁在干活。按钮、菜单、快捷键、脚本、定时器——所有触发源都变成"同一句 execute()",业务对象换掉或重构,触发源一行不改。
扩展点:加一个命令类。新增功能 = 新增一个 ConcreteCommand 子类 + 在组装处 new 一下。既有代码全部满足 OCP——对扩展开放,对修改关闭。这正是命令模式最值钱的地方:功能增长从"改代码"变成"加代码"。
依赖倒置(DIP):高层模块(Invoker)依赖抽象(Command),低层模块(Receiver)也通过命令被间接引用——两者都面向接口编程,方向彻底反转。
撤销 / 重做成为"免费"能力:命令自带 undo(),历史栈只负责"弹出栈顶、调用方法",完全不懂业务。撤销的复杂度从业务代码中被剥离,集中到框架(QUndoStack)里。
请求可被存储、排队、组合、传输:命令是对象——可以被放进队列延迟执行(任务调度)、可以组合成宏命令(组合模式+命令模式的经典合作)、可以序列化后通过网络传输(远程命令/操作审计日志)。这是直接函数调用永远给不了的能力。
可测试性:每个命令是独立单元,构造时注入接收者(可用 mock 替换),单测覆盖 redo/undo 的成对性即可;界面逻辑可以用假命令替换验证。
① switch/if-else 膨胀:为了区分"点的是哪个按钮",事件处理函数里堆满 if (type == ADD) ... else if (type == DELETE) ...,每加一个操作就多一个分支,代码很快变成"面条";
② 三个入口三份重复代码:菜单、工具栏、快捷键指向同一个操作,却要写三遍调用逻辑。改一处忘了另外两处,Bug 就来了(QAction 正是为消灭这种重复而生);
③ 撤销功能无从下手:没有命令对象就没有"上一步"的记录。硬做撤销只能保存整份状态快照——大文档下内存爆炸、比较状态差异极其痛苦;
④ 界面与业务硬耦合:窗口类直接持有业务对象并调用其方法,业务接口一变,所有界面代码跟着改;测试界面时无法替换真实业务对象;
⑤ 高级能力全部缺失:宏录制、操作日志、事务回滚、远程指令——这些全都要求"操作是对象",没有命令模式就永远做不了。QGIS、Blender、PhotoShop 的"动作/宏"面板,底层全是命令对象列表。
适合使用(3-5 个场景):
· 需要撤销 / 重做:编辑器、绘图程序、CAD——这是命令模式最经典的应用;
· 同一操作有多个触发入口:菜单项、工具栏按钮、快捷键、右键菜单都触发同一个动作(Qt 里用 QAction 承载);
· 需要把操作排队或延迟执行:任务队列、定时批量操作、离线指令同步——命令对象可以入队、可序列化;
· 需要宏录制 / 脚本回放:把用户操作录成命令序列,一键重放;
· 需要操作日志与审计:金融交易、运维系统把每条操作作为命令记录入库,可追溯、可回滚。
不适合使用(2-4 个场景):
· 操作只有一两个、永远不会撤销、也不需要排队——直接调用函数更简单直接;
· 操作无状态且极其简单(如纯 getter/查询)——封装成命令纯属绕路;
· 团队不熟悉该模式时用于所有调用点——反而增加理解成本;
· 性能极端敏感的热路径(每秒百万次的简单调用)——多一层虚函数分发有微小开销。
过度设计提醒:如果某个类只实现一个命令、且没有撤销/队列/宏/日志需求,引入 Command 只是多了一层间接性。先问自己三个问题:要撤销吗?要排队吗?有多入口吗?三个都否——请用普通函数或 lambda。
命令模式 vs 策略模式:两者结构几乎一样(都是"一个接口 + 多个实现 + 一个持有者"),但意图完全不同。策略封装的是算法(怎么算:排序用快排还是归并),命令封装的是请求(做什么、对谁做、参数是什么);策略通常没有撤销,命令天生带 undo;策略侧重"运行时替换算法族",命令侧重"请求的封装与延迟执行"。混淆判据:问自己"这个对象是做法还是一件事"——做法是策略,一件事是命令。
命令模式 vs 责任链模式:责任链让请求在一串处理者之间"漂流",由链上的处理者决定谁接手;命令模式则把请求固化为一个对象,由 Invoker 直接触发、Receiver 执行。责任链解耦的是"发送者与一串潜在处理者",命令解耦的是"发送者与一个执行者"。两者也可以结合:命令对象作为请求沿责任链传递。
命令撤销 vs 备忘录模式:两种撤销策略。命令用逆操作(undo 时执行相反动作,快、省内存,但要求每个操作都可逆);备忘录用状态快照(undo 时恢复保存的旧状态,通用但费内存)。Photoshop 的图层历史用快照式(Ctrl+Z 整层回滚),文本输入用逆操作式(删掉刚打的字)。Qt 的 QUndoStack 走命令路线。
轻量替代:C++ 中 std::function + lambda 就是"贫民版命令"。只做延迟执行时完全够用;一旦需要 undo/redo、命令合并、命令组合(宏),就必须升级为完整命令类——这也是 QUndoCommand 存在的原因。
① QUndoCommand / QUndoStack(Qt GUI 模块)——命令模式的标准教科书实现:
· QUndoCommand 暴露 redo() / undo() 纯虚函数(对应 Command 接口),setText() 提供用户可读描述;
· 支持命令压缩:子类实现 id() 和 mergeWith(),连续输入"abc"可以合并成一条命令,撤销一次全删——这是真实产品级撤销的必备能力;
· 支持宏命令:给命令传 QUndoCommand* parent,父子命令在栈中作为一个整体撤销/重做(组合模式的运用);
· QUndoStack 默认容量 100 步,setUndoLimit() 可调;createUndoAction() / createRedoAction() 自动生成带快捷键、随栈状态自动 enabled/disabled 的 QAction。
② QAction —— "动作"的命令化封装:一个 QAction 对象同时挂在菜单、工具栏、快捷键三处,任何一处触发都发射同一个 triggered() 信号——消灭了三入口重复代码;setCheckable() 让动作自带状态,配合 QActionGroup 实现互斥。可以说 QAction 是"命令 + 状态 + 用户界面描述"三合一。
③ QEvent 分发体系(QApplication::notify)——命令的变体:键盘/鼠标事件被封装成 QEvent 对象,由 Qt 事件循环这个"超级 Invoker"分发给目标对象——请求对象化的思想贯穿 Qt 事件系统。
④ 轻量命令的广泛使用:QTimer::singleShot、QtConcurrent::run、QObject::connect 的 functor 重载——全部接受 std::function,即"延迟执行/排队执行"的轻量命令形态。
Q1:命令模式解决什么问题?有哪几个核心角色?
A:把"请求"封装成对象,使请求发送者(Invoker)与请求执行者(Receiver)解耦,从而支持撤销/重做、排队、日志、宏等能力。核心角色:Command(抽象命令)、ConcreteCommand(具体命令,绑定接收者与参数)、Receiver(接收者,真正干活)、Invoker(调用者,触发执行)、Client(组装者)。
Q2:命令模式如何实现撤销?undo 的两种策略是什么?
A:每个命令实现 undo(),历史栈弹出栈顶命令并调用之。两种策略:逆操作式(undo 执行相反动作,如追加→删除,快、省内存,要求操作可逆);状态快照式(undo 恢复保存的旧状态,通用但费内存)。命令模式天然适合逆操作式。
Q3:命令模式和策略模式有什么区别?
A:意图不同:策略封装"算法/做法"(怎么算),命令封装"请求/事件"(做什么、对谁做)。策略侧重运行时替换算法族,通常无撤销;命令侧重请求的封装、延迟执行与撤销。判据:这个对象是"做法"还是"一件事"。
Q4:QUndoStack::push() 做了什么?为什么 push 后不用手动调用 redo?
A:push() 把命令压入撤销栈(清空重做栈),并自动调用命令的 redo() 完成首次执行;同时把命令所有权收归栈管理(超限自动 delete)。所以调用方只需 push,执行与内存管理都由框架完成。这是"Invoker + 历史"合一的典型设计。
Q5:命令模式有什么缺点?如何缓解?
A:主要缺点是类数量膨胀——每个操作一个类,操作一多类爆炸。缓解:① 无撤销需求时用 std::function + lambda 轻量替代;② 用命令合并(id() + mergeWith())减少历史条目;③ 用宏命令(命令组合)聚合细粒度操作;④ 泛型命令模板(模板化参数)批量生成相似命令。
练习:可撤销计算器(约 20 分钟)
用命令模式实现一个支持 + - × ÷ 四则运算的计算器:每次按键(如"3"、"+"、"5"、"=")都封装为一个命令对象;维护一个最多 10 步的撤销历史栈,提供 undo():按一次撤销回上一步的显示值。做完后思考:连续按 8 次撤销会发生什么?为什么需要上限?
提示 1:undo 有两种实现——每个命令保存"操作前的值"(快照式,最简单),或保存操作数与符号、用逆运算恢复(逆操作式)。先快照式跑通,再想逆操作式。
提示 2:用 std::vector<std::unique_ptr<Command>> 当栈;新操作先执行再 push_back;撤销时先调用 undo() 再 pop_back();容量超 10 用 erase(begin()) 丢弃最老的命令(注意被丢弃的命令不需要再撤销——它已经不在栈里了)。