命令模式(Command)是行为型设计模式的一种,它通过将请求封装为对象,使请求的发送者与接收者解耦,同时支持请求的参数化、队列化、日志记录和撤销操作。这种模式将"做什么"与"谁去做"分离,提高了系统的灵活性和可扩展性。
一、核心思想与角色
命令模式的核心是"封装请求为对象",通过命令对象介导发送者与接收者的交互。其核心角色如下:
角色名称 | 核心职责 |
---|---|
抽象命令(Command) | 定义命令的接口,声明执行命令的方法(如execute() )和撤销方法(如undo() )。 |
具体命令(ConcreteCommand) | 实现抽象命令接口,持有接收者对象,在execute() 中调用接收者的具体方法,完成请求。 |
接收者(Receiver) | 执行命令的实际对象,包含具体的业务逻辑(如开灯、关灯的具体实现)。 |
调用者(Invoker) | 持有命令对象,负责调用命令的execute() 方法,不关心命令如何执行。 |
客户端(Client) | 创建具体命令对象,指定其接收者,并将命令对象交给调用者执行。 |
核心思想:将请求封装为命令对象,使发送者(调用者)无需知道接收者的具体信息,只需通过命令对象即可触发请求;同时,命令对象可被存储、传递和复用,支持复杂的操作管理(如撤销、重试)。
二、实现示例(智能家电控制系统)
假设我们需要设计一个智能遥控器(调用者),可控制多种家电(接收者,如灯光、电视)的开关操作,并支持撤销功能。使用命令模式可灵活扩展设备类型和操作:
cpp
#include <iostream>
#include <string>
#include <vector>
// 2. 接收者1:灯光
class Light {
private:
std::string location; // 位置(如客厅、卧室)
public:
Light(const std::string& loc) : location(loc) {}
// 具体业务逻辑:开灯
void on() {
std::cout << location << "灯光已打开" << std::endl;
}
// 具体业务逻辑:关灯
void off() {
std::cout << location << "灯光已关闭" << std::endl;
}
};
// 2. 接收者2:电视
class TV {
private:
std::string location;
int channel; // 频道
public:
TV(const std::string& loc) : location(loc), channel(0) {}
void on() {
std::cout << location << "电视已打开" << std::endl;
}
void off() {
std::cout << location << "电视已关闭" << std::endl;
}
void setChannel(int ch) {
channel = ch;
std::cout << location << "电视已切换到频道" << channel << std::endl;
}
int getChannel() const { return channel; }
};
// 1. 抽象命令
class Command {
public:
virtual void execute() = 0; // 执行命令
virtual void undo() = 0; // 撤销命令
virtual ~Command() = default;
};
// 3. 具体命令1:开灯命令
class LightOnCommand : public Command {
private:
Light* light; // 持有接收者(灯光)
public:
LightOnCommand(Light* l) : light(l) {}
// 执行:调用接收者的on()
void execute() override {
light->on();
}
// 撤销:调用接收者的off()(与execute相反)
void undo() override {
light->off();
}
};
// 3. 具体命令2:关灯命令
class LightOffCommand : public Command {
private:
Light* light;
public:
LightOffCommand(Light* l) : light(l) {}
void execute() override {
light->off();
}
void undo() override {
light->on();
}
};
// 3. 具体命令3:开电视并切换频道命令
class TVOnWithChannelCommand : public Command {
private:
TV* tv;
int prevChannel; // 记录之前的频道(用于撤销)
public:
TVOnWithChannelCommand(TV* t, int channel) : tv(t), prevChannel(0) {
this->prevChannel = tv->getChannel(); // 保存当前频道
}
void execute() override {
prevChannel = tv->getChannel(); // 执行前再次保存当前频道
tv->on();
tv->setChannel(5); // 切换到5频道
}
void undo() override {
tv->setChannel(prevChannel); // 恢复到之前的频道
tv->off(); // 关闭电视
}
};
// 3. 空命令(用于初始化遥控器按钮,避免空指针)
class NoCommand : public Command {
public:
void execute() override {} // 什么也不做
void undo() override {} // 什么也不做
};
// 4. 调用者:智能遥控器
class RemoteControl {
private:
std::vector<Command*> onCommands; // 存储"开"命令
std::vector<Command*> offCommands; // 存储"关"命令
Command* lastCommand; // 记录最后执行的命令(用于撤销)
public:
// 构造函数:初始化按钮为无命令
RemoteControl(int buttonCount) {
Command* noCommand = new NoCommand();
for (int i = 0; i < buttonCount; ++i) {
onCommands.push_back(noCommand);
offCommands.push_back(noCommand);
}
lastCommand = noCommand;
}
// 设置按钮对应的命令
void setCommand(int slot, Command* onCmd, Command* offCmd) {
if (slot >= 0 && slot < onCommands.size()) {
onCommands[slot] = onCmd;
offCommands[slot] = offCmd;
}
}
// 按下"开"按钮
void pressOnButton(int slot) {
if (slot >= 0 && slot < onCommands.size()) {
onCommands[slot]->execute();
lastCommand = onCommands[slot]; // 记录最后执行的命令
}
}
// 按下"关"按钮
void pressOffButton(int slot) {
if (slot >= 0 && slot < offCommands.size()) {
offCommands[slot]->execute();
lastCommand = offCommands[slot]; // 记录最后执行的命令
}
}
// 按下撤销按钮
void pressUndoButton() {
std::cout << "撤销操作:";
lastCommand->undo();
}
// 析构函数:释放命令对象
~RemoteControl() {
// 释放所有命令(注意:NoCommand可能被多个按钮共享,这里简化处理)
for (auto cmd : onCommands) {
delete cmd;
}
// offCommands中的命令可能与onCommands重复,避免重复释放
lastCommand = nullptr;
}
};
// 客户端代码:配置遥控器并使用
int main() {
// 创建接收者
Light* livingRoomLight = new Light("客厅");
TV* livingRoomTV = new TV("客厅");
// 创建具体命令(绑定接收者)
Command* lightOn = new LightOnCommand(livingRoomLight);
Command* lightOff = new LightOffCommand(livingRoomLight);
Command* tvOn = new TVOnWithChannelCommand(livingRoomTV, 5);
// 创建调用者(遥控器,2个按钮)
RemoteControl* remote = new RemoteControl(2);
// 配置按钮:按钮0控制灯光,按钮1控制电视
remote->setCommand(0, lightOn, lightOff);
remote->setCommand(1, tvOn, new LightOffCommand(livingRoomLight)); // 简化:电视关闭复用灯光关闭命令
// 操作遥控器
std::cout << "=== 按下客厅灯光开按钮 ===" << std::endl;
remote->pressOnButton(0);
std::cout << "\n=== 按下客厅电视开按钮 ===" << std::endl;
remote->pressOnButton(1);
std::cout << "\n=== 按下撤销按钮 ===" << std::endl;
remote->pressUndoButton();
std::cout << "\n=== 按下客厅灯光关按钮 ===" << std::endl;
remote->pressOffButton(0);
std::cout << "\n=== 按下撤销按钮 ===" << std::endl;
remote->pressUndoButton();
// 释放资源
delete remote;
delete livingRoomTV;
delete livingRoomLight;
return 0;
}
三、代码解析
-
接收者(Receiver):
Light
和TV
是具体的家电,包含实际的业务逻辑(on()
、off()
等),它们不知道命令的存在,只负责执行具体操作。
-
抽象命令(Command) :
定义了
execute()
(执行)和undo()
(撤销)接口,所有具体命令都需实现这两个方法。 -
具体命令(ConcreteCommand):
- 每个命令绑定一个接收者(如
LightOnCommand
绑定Light
),在execute()
中调用接收者的对应方法(如light->on()
)。 undo()
方法实现与execute()
相反的操作(如开灯的撤销是关灯),对于复杂命令(如TVOnWithChannelCommand
),需要记录执行前的状态(如之前的频道)用于恢复。
- 每个命令绑定一个接收者(如
-
调用者(Invoker) :
RemoteControl
(遥控器)持有多个命令对象,提供按钮操作接口(pressOnButton()
、pressUndoButton()
),通过调用命令的execute()
或undo()
完成操作,无需知道具体的接收者和执行细节。 -
空命令(NoCommand) :
作为默认命令初始化遥控器按钮,避免空指针异常,体现了"null对象模式"的思想。
-
客户端使用 :
客户端负责创建接收者、命令和调用者,将命令绑定到调用者的按钮上,最终通过调用者触发命令执行。
四、核心优势与适用场景
优势
- 解耦发送者与接收者:调用者无需知道接收者的具体类型和操作细节,只需通过命令对象交互。
- 支持命令队列和日志:命令对象可被存储在队列中批量执行,或记录到日志中实现故障恢复(如数据库事务日志)。
- 支持撤销/重做 :通过
undo()
和redo()
方法,可实现操作的撤销和重复执行(需记录命令历史)。 - 易于扩展 :新增命令只需实现
Command
接口,无需修改现有调用者和接收者(符合开闭原则)。 - 参数化操作 :可通过命令对象传递参数(如
TVOnWithChannelCommand
中的频道),使操作更灵活。
适用场景
- 需要抽象出操作并参数化:如GUI中的菜单操作、遥控器按钮、数据库事务。
- 需要支持撤销/重做:如文本编辑器的撤销功能、绘图软件的操作回退。
- 需要将操作队列化或日志化:如任务调度系统、命令批处理、操作日志记录。
- 需要解耦请求发送者和接收者:如分布式系统中的命令分发、中间件的事件处理。
五、与其他模式的区别
模式 | 核心差异点 |
---|---|
命令模式 | 将请求封装为对象,支持队列、日志、撤销,强调"请求的封装与管理"。 |
职责链模式 | 请求沿链传递,由第一个能处理的对象处理,强调"请求的分发与传递"。 |
策略模式 | 封装算法家族,使算法可动态替换,强调"算法的选择与切换"。 |
观察者模式 | 一个对象改变时通知多个观察者,强调"一对多的依赖关系与事件通知"。 |
六、实践建议
- 设计完善的撤销机制 :对于需要撤销的命令,确保
undo()
方法能准确恢复到执行前的状态,必要时在execute()
中记录历史状态。 - 使用空命令避免空指针 :如示例中的
NoCommand
,简化调用者的空判断逻辑。 - 合理管理命令生命周期:命令对象可能被长期存储(如日志),需注意内存管理,避免资源泄漏。
- 结合备忘录模式 :对于复杂状态的撤销,可在命令中使用备忘录模式存储对象状态,简化
undo()
实现。
命令模式的核心价值在于"将请求标准化、对象化",通过命令对象实现了请求发送与执行的解耦,同时为复杂操作管理(如队列、日志、撤销)提供了灵活的解决方案。在需要对操作进行抽象、扩展和精细化管理的场景中,命令模式是一种非常有效的设计选择。