命令模式:把请求打包成对象
命令模式,说白了就是把「你要做什么」这件事,封装成一个独立的对象。我刚开始接触这个模式时,觉得有点绕——好好的方法调用不用,非得包一层?后来在项目中踩过坑,才明白它的价值。
举个例子。你写了一个遥控器程序,有开灯、关灯、开空调、关空调四个按钮。最直接的做法,就是每个按钮的点击事件里直接调用对应的方法。但问题来了——如果我想加一个「撤销」功能呢?或者我想把一系列操作录制成宏,一键执行?这时候,命令模式就派上用场了。
定义与核心思想
命令模式(Command Pattern)属于行为型设计模式。它将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
嗯,这里要注意:核心就是把「行为调用者」和「行为执行者」彻底解耦。调用者不需要知道谁去执行、怎么执行,它只需要发出命令就行了。
结构组成
命令模式的结构其实很清晰,就四个角色:
| 角色 | 职责 | 我见过的常见实现 |
|---|---|---|
| Command(抽象命令) | 声明执行操作的接口 | 通常是一个接口或抽象类,包含 execute() 方法 |
| ConcreteCommand(具体命令) | 绑定接收者与具体操作 | 持有接收者对象的引用,在 execute() 中调用接收者的方法 |
| Invoker(调用者) | 要求命令对象执行请求 | 比如遥控器、菜单项、任务队列 |
| Receiver(接收者) | 真正执行业务逻辑的对象 | 比如灯、空调、数据库连接 |
关键点:调用者只跟 Command 接口打交道,不关心具体是谁在执行。接收者也可以完全不知道命令对象的存在。
核心逻辑流程图
下面这张图,是我自己画的一个命令模式执行流程。你看一眼就明白了:
代码实现
我用一个智能家居的遥控器场景来演示。你想想看,这应该是命令模式最经典的例子了。
// 1. 接收者:灯
class Light {
public void turnOn() {
System.out.println("灯亮了");
}
public void turnOff() {
System.out.println("灯灭了");
}
}
// 2. 抽象命令接口
interface Command {
void execute();
void undo(); // 支持撤销
}
// 3. 具体命令:开灯
class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
@Override
public void undo() {
light.turnOff();
}
}
// 4. 具体命令:关灯
class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
@Override
public void undo() {
light.turnOn();
}
}
// 5. 调用者:遥控器
class RemoteControl {
private Command slot;
private Command lastCommand; // 记录上一条命令用于撤销
public void setCommand(Command command) {
this.slot = command;
}
public void pressButton() {
slot.execute();
lastCommand = slot;
}
public void pressUndo() {
if (lastCommand != null) {
lastCommand.undo();
}
}
}
// 6. 客户端使用
public class Client {
public static void main(String[] args) {
Light livingRoomLight = new Light();
Command lightOn = new LightOnCommand(livingRoomLight);
Command lightOff = new LightOffCommand(livingRoomLight);
RemoteControl remote = new RemoteControl();
// 开灯
remote.setCommand(lightOn);
remote.pressButton(); // 输出:灯亮了
// 撤销
remote.pressUndo(); // 输出:灯灭了
}
}
我的经验:在实际项目中,我习惯把 undo 操作也设计成命令模式的一部分。这样撤销功能就不需要额外维护状态栈了,每个命令自己知道怎么回退。
优缺点分析
任何模式都有两面性。命令模式也不例外。
| 优点 | 缺点 |
|---|---|
| 彻底解耦调用者与接收者 | 每增加一个命令就要新建一个类,类数量膨胀 |
| 容易实现撤销和重做 | 如果命令对象持有大量状态,内存开销会增大 |
| 支持命令的排队、记录、日志 | 对于简单场景,过度设计 |
| 可以组合命令形成宏命令 | 调试时命令链可能变长,追踪困难 |
避坑指南:我曾经在一个项目中,把每个数据库操作都封装成命令对象。结果一个页面加载要创建几十个命令实例,内存直接飙上去。后来才意识到——命令模式适合「需要记录、撤销、排队」的场景,不是所有方法调用都要包一层。
应用场景
什么时候该用命令模式?我总结了几个典型场景:
- 需要支持撤销/重做:比如文本编辑器的 Ctrl+Z、画图软件的撤销操作
- 需要记录操作日志:比如金融系统的交易记录,每个操作都要持久化
- 需要异步执行或任务队列:比如线程池的任务提交,每个任务就是一个命令对象
- 需要宏命令/组合命令:比如游戏中的连招系统,一键执行多个操作
- 需要将请求参数化:比如菜单系统,每个菜单项对应一个命令对象
我记得有一次做电商后台的订单处理系统,需要支持「批量操作」——比如批量发货、批量退款。每个操作都要记录日志,还要支持部分撤销。用命令模式实现后,代码结构清晰了很多,后来加新功能也只需要新增一个命令类,完全不影响现有代码。
与策略模式的区别
很多初学者会把命令模式和策略模式搞混。我简单说一下:
- 策略模式:关注「怎么做」,不同的算法可以互相替换。比如排序算法,你可以选快排或归并。
- 命令模式:关注「做什么」,把请求封装成对象,支持排队、撤销、记录。比如遥控器按钮,按了就执行。
说白了,策略模式是算法的封装,命令模式是请求的封装。两者目的完全不同。
一句话总结:命令模式就是把「请求」变成「对象」,让请求可以被传递、存储、撤销、组合。它解决的核心问题是——如何让调用者不需要知道接收者的任何细节。
嗯,命令模式就讲到这里。如果你在实际项目中用过这个模式,欢迎交流你的踩坑经历。