命令模式:从撤销操作到分布式调度的命令封装实践
一、模式核心:将请求封装为可操作的 “命令对象”
在文本编辑器中,“撤销” 功能需要记录每一步操作;在分布式系统中,远程调用需要将请求序列化为可传输的对象。这类场景的核心需求是:将 “请求” 与 “执行” 解耦,使请求可被记录、撤销、重试。** 命令模式(Command Pattern)** 通过将请求封装为独立的命令对象,实现发送者与执行者的解耦,核心解决:
- 请求参数化:将操作封装为对象(如
SaveCommand
、UndoCommand
) - 操作可追溯:支持日志记录、撤销(Undo)、重做(Redo)等高级功能
核心思想与 UML 类图
命令模式通过 “命令对象” 解耦请求发送者和接收者,形成可扩展的操作链条:
二、核心实现:构建支持撤销的计算器
1. 定义命令接口(封装操作)
public interface Command {
void execute(); // 执行命令
void undo(); // 撤销命令(可选)
}
2. 实现具体命令(绑定接收者与操作逻辑)
加法命令
public class AddCommand implements Command {
private Calculator receiver; // 接收者(实际执行操作的对象)
private int operand; // 操作数
private int previousResult; // 保存旧结果用于撤销
public AddCommand(Calculator receiver, int operand) {
this.receiver = receiver;
this.operand = operand;
this.previousResult = receiver.getResult(); // 记录执行前的状态
}
@Override
public void execute() {
receiver.add(operand);
}
@Override
public void undo() {
receiver.setResult(previousResult); // 恢复执行前的状态
}
}
减法命令
public class SubtractCommand implements Command {
private Calculator receiver;
private int operand;
private int previousResult;
public SubtractCommand(Calculator receiver, int operand) {
this.receiver = receiver;
this.operand = operand;
this.previousResult = receiver.getResult();
}
@Override
public void execute() {
receiver.subtract(operand);
}
@Override
public void undo() {
receiver.setResult(previousResult);
}
}
3. 接收者(实际执行操作的实体)
public class Calculator {
private int result;
public void add(int num) {
result += num;
}
public void subtract(int num) {
result -= num;
}
public int getResult() {
return result;
}
public void setResult(int result) {
this.result = result;
}
}
4. 调用者(触发命令执行)
public class Invoker {
private Command command;
private Command undoCommand; // 记录上一条命令用于撤销
public void setCommand(Command cmd) {
this.command = cmd;
}
public void execute() {
if (command != null) {
command.execute();
undoCommand = command; // 保存当前命令用于撤销
}
}
public void undo() {
if (undoCommand != null) {
undoCommand.undo();
}
}
}
5. 客户端调用示例(支持撤销的计算)
public class ClientDemo {
public static void main(String[] args) {
Calculator calculator = new Calculator();
Invoker invoker = new Invoker();
// 执行加法命令
Command addCmd = new AddCommand(calculator, 10);
invoker.setCommand(addCmd);
invoker.execute();
System.out.println("当前结果:" + calculator.getResult()); // 输出:10
// 执行减法命令
Command subtractCmd = new SubtractCommand(calculator, 5);
invoker.setCommand(subtractCmd);
invoker.execute();
System.out.println("当前结果:" + calculator.getResult()); // 输出:5
// 撤销上一步操作(减法)
invoker.undo();
System.out.println("撤销后结果:" + calculator.getResult()); // 输出:10
}
}
三、进阶:构建支持批处理与日志的命令队列
1. 命令队列(支持批量执行与撤销)
public class CommandQueue {
private List<Command> commandList = new ArrayList<>();
public void addCommand(Command cmd) {
commandList.add(cmd);
}
public void executeAll() {
commandList.forEach(Command::execute);
}
public void undoAll() {
// 逆序撤销(先进后撤销)
for (int i = commandList.size() - 1; i >= 0; i--) {
commandList.get(i).undo();
}
}
}
// 使用示例:批量转账命令
CommandQueue queue = new CommandQueue();
queue.addCommand(new TransferCommand(bankA, bankB, 1000));
queue.addCommand(new TransferCommand(bankB, bankC, 500));
queue.executeAll(); // 批量执行
queue.undoAll(); // 批量撤销
2. 持久化命令日志(支持故障恢复)
public class CommandLogger {
private FileWriter writer;
public CommandLogger(String logFile) throws IOException {
writer = new FileWriter(logFile, true);
}
public void logCommand(Command cmd) {
// 序列化命令到日志文件(需命令实现Serializable)
writer.write(cmd.getClass().getName() + "\n");
writer.write(cmd.getState() + "\n"); // 假设命令有获取状态的方法
writer.flush();
}
public List<Command> readCommands() throws IOException {
// 从日志恢复命令(需反序列化)
List<Command> commands = new ArrayList<>();
// 省略具体实现...
return commands;
}
}
3. 可视化命令流程
四、框架与源码中的命令模式实践
1. Spring MVC 的 Command 对象
- 核心类:
CommandObject
(如表单提交对象) - 原理:将 HTTP 请求参数封装为命令对象,解耦控制器与请求处理逻辑
// 控制器接收命令对象
@PostMapping("/order")
public String processOrder(@ModelAttribute OrderCommand command) {
orderService.process(command); // 命令对象包含所有处理所需参数
return "success";
}
2. Struts 2 的 Action 机制
Action
接口是典型的命令对象,封装请求处理逻辑- 支持
execute()
(执行)和validate()
(校验),通过Result
定义后续流程
3. Swing 事件处理(隐式命令模式)
- 按钮点击事件的
ActionListener
本质是命令对象
JButton saveButton = new JButton("保存");
saveButton.addActionListener(e -> {
// 命令的execute()逻辑
saveFile();
});
4. 分布式系统中的远程命令
场景:将命令序列化为 JSON/Protobuf,通过 RPC 发送到远程节点执行
关键实现:
// 命令需实现Serializable public class RemoteCommand implements Serializable { private String methodName; private Object[] params; // 省略getter/setter } // 远程调用者 public Object executeRemoteCommand(RemoteCommand cmd) throws Exception { // 通过Socket/RestTemplate发送命令到服务端 return remoteService.invoke(cmd); }
五、避坑指南:正确使用命令模式的 3 个要点
1. 避免过度封装简单操作
- ❌ 反模式:对单一方法调用(如
delete()
)强行封装命令 - ✅ 最佳实践:仅在需要撤销、日志、批处理等高级功能时使用
2. 处理撤销的幂等性
- 撤销操作需保证幂等(多次撤销不影响结果),建议:
- 记录命令执行前的完整状态(如计算器的
previousResult
) - 使用版本号或时间戳确保撤销的正确性
- 记录命令执行前的完整状态(如计算器的
3. 控制命令类的数量
- 大量具体命令可能导致类爆炸,建议:
- 使用参数化命令(如
ParametricCommand
支持不同操作数) - 结合工厂模式统一管理命令创建
- 使用参数化命令(如
4. 反模式:混淆命令与业务逻辑
- 命令对象应聚焦 “如何执行操作”,而非 “操作本身的业务规则”
- 复杂业务逻辑应封装在接收者(Receiver)或独立服务中
六、总结:何时该用命令模式?
适用场景 | 核心特征 | 典型案例 |
---|---|---|
需要支持撤销 / 重做功能 | 操作具有可逆性,需记录历史状态 | 文本编辑器(Ctrl+Z)、版本控制系统 |
分布式远程调用 | 请求需序列化传输,支持异步执行 | RPC 框架、消息队列(MQ) |
批处理与事务性操作 | 需要批量执行并保证原子性 | 银行转账(批量操作回滚) |
解耦请求发送与具体执行 | 发送者与执行者需要松耦合 | 事件驱动架构、任务调度系统 |
命令模式通过 “请求封装 + 责任分离” 的设计,将操作的 “触发” 与 “执行” 解耦,为系统添加了强大的扩展性(如支持撤销、日志、分布式调度)。下一篇我们将深入探讨迭代器模式,解析如何统一不同数据结构的遍历方式,敬请期待!
扩展思考:命令模式 vs 策略模式
两者都通过封装实现解耦,但核心目标不同:
模式 | 封装对象 | 核心功能 | 状态管理 |
---|---|---|---|
命令模式 | 具体请求(如 “保存”“撤销”) | 支持请求的记录与回退 | 需保存执行前 / 后状态 |
策略模式 | 算法或策略(如 “排序算法”) | 动态切换算法实现 | 无状态或仅共享上下文 |
理解这种差异,能帮助我们在设计时选择更合适的模式来解决实际问题。