如何手写一个命令模式?核心代码怎么设计?
简化版
手写命令模式通常先定义 Command 接口,再实现具体命令类,让命令类持有真正的业务执行者 Receiver,最后由 Invoker 保存并触发命令。需要撤销时,可以在命令接口里增加 undo(),并用栈保存执行历史。
详细版
一个标准命令模式可以按五步写:
- 定义命令接口;
- 定义真正执行业务的 Receiver;
- 为每个操作实现具体命令;
- 让 Invoker 持有命令并触发;
- 如果需要撤销,记录历史命令并实现
undo()。
简化代码:
interface Command {
void execute();
void undo();
}
class LightOnCommand implements Command {
private final Light light;
LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
public void undo() {
light.off();
}
}
命令模式的实现重点不是代码难度,而是职责划分:Invoker 不写业务,Command 封装请求,Receiver 负责真实动作。
完整版教学
一、定义命令接口
先抽象出统一命令:
interface Command {
void execute();
void undo();
}
如果业务不需要撤销,undo() 可以不定义。但面试里命令模式经常和撤销重做一起考,所以加上更完整。
二、定义接收者 Receiver
Receiver 是真正执行业务的对象。
class Light {
void on() {
System.out.println("灯打开了");
}
void off() {
System.out.println("灯关闭了");
}
}
这里的 Light 可以替换成真实项目里的 Service、Repository、SDK Client 或领域对象。
三、实现具体命令
打开灯命令:
class LightOnCommand implements Command {
private final Light light;
LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.on();
}
public void undo() {
light.off();
}
}
关闭灯命令:
class LightOffCommand implements Command {
private final Light light;
LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.off();
}
public void undo() {
light.on();
}
}
每个命令都知道自己怎么执行,也知道如何反向恢复。
四、设计调用者 Invoker
Invoker 只负责保存和触发命令。
class RemoteControl {
private Command command;
void setCommand(Command command) {
this.command = command;
}
void pressButton() {
command.execute();
}
}
这个遥控器可以控制任意命令,而不是写死控制灯。
五、加入撤销栈
如果要支持撤销,可以记录最近执行过的命令。
class RemoteControl {
private Command command;
private final Deque<Command> history = new ArrayDeque<>();
void setCommand(Command command) {
this.command = command;
}
void pressButton() {
command.execute();
history.push(command);
}
void undo() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
这就是很多编辑器撤销功能的基础思路。
六、工程实现里要关注失败处理
真实项目中 execute() 可能失败。比如发起支付、扣库存、调用远程接口,都可能出现异常。
这时要考虑:
- 执行失败的命令是否入历史;
undo()是否一定可靠;- 是否需要幂等键;
- 是否需要重试;
- 是否要记录命令执行日志;
- 是否要持久化命令状态。
命令模式只是结构,不能替代事务、消息可靠性和异常治理。
七、常见误区与追问
验收命令模式不能只看类名,而要验证调用者是否只依赖 Command、接收者是否保留业务能力。以“开灯、关灯”2 个命令为例,新增“调暗”时应只新增命令实现,不修改按钮的触发流程。若 Invoker 仍按命令类型写 if/else,对象化请求并没有真正带来扩展性。
| 检查维度 | 判定依据 |
|---|---|
| 新增请求 | 新增 Command,Invoker 不变 |
| 业务执行 | 委托 Receiver,不塞进 Invoker |
记忆钩子:Invoker 管“何时触发”,Command 管“触发什么”,Receiver 管“具体怎么做”。
- 误区:Command 必须是无状态对象。 撤销、审计或重试往往要求它保存参数与执行前快照;关键是状态边界清晰。
- 追问:为什么不让按钮直接调用 Receiver? 直接调用会把界面控件与业务接口绑死,也无法统一排队、记录和撤销。
- 误区:execute 中不能写任何业务逻辑。 它可以做参数组织与调用编排,但核心领域规则仍应留在 Receiver 或领域服务。
- 追问:命令对象应该复用还是每次新建? 无状态命令可以复用;携带请求参数、快照或执行结果的命令通常一次请求一个实例。
- 追问:如何证明实现符合开闭原则? 加入第 3 种命令并运行原有 Invoker 测试,若无需修改分派代码才算成立。
八、加强记忆
手写命令模式抓住三件事:Command 定义统一操作,ConcreteCommand 持有 Receiver 并封装请求,Invoker 只负责触发命令。需要撤销时给命令补 undo(),并用历史栈管理已执行命令。