命令模式:请求封装、撤销重做、实战:事务回滚
命令模式,说白了就是把「请求」包装成一个对象。
你想想看,在代码里我们经常要调用某个方法。但方法调用是紧耦合的——调用方直接依赖了执行方。如果我想把请求排队、记录日志、或者支持撤销,那就麻烦了。
命令模式就是来解决这个问题的。它把「做什么」和「谁来做」彻底拆开。
为什么需要命令模式?
我在项目中遇到过这样一个场景:一个编辑器,用户点了「保存」按钮,然后想撤销。如果保存逻辑直接写在按钮点击事件里,撤销就无从谈起。
命令模式的做法是:每次操作都生成一个命令对象,里面包含了执行和撤销两个方法。按钮只管调用命令的 execute(),撤销时调用 undo()。
命令模式的结构
命令模式有四个角色,我习惯这样记:
- Command(命令接口):声明 execute() 和 undo() 方法
- ConcreteCommand(具体命令):实现命令接口,持有接收者引用
- Receiver(接收者):真正干活的对象
- Invoker(调用者):持有命令对象,触发执行
嗯,这里要注意:调用者不关心命令内部怎么实现,它只负责调用 execute()。
一个简单的例子:遥控器
先看一个最基础的实现。假设我们有个遥控器,可以控制灯的开和关。
// 命令接口
public interface Command {
void execute();
void undo();
}
// 接收者:灯
public class Light {
public void on() { System.out.println("灯亮了"); }
public void off() { System.out.println("灯灭了"); }
}
// 具体命令:开灯
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
@Override
public void undo() {
light.off();
}
}
// 调用者:遥控器
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
你看,遥控器根本不知道它控制的是灯还是风扇。它只认 Command 接口。
撤销与重做:核心难点
撤销重做,说白了就是「历史记录」的管理。
我习惯用一个栈来保存执行过的命令。撤销时从栈里弹出命令,调用它的 undo()。重做时再压回去。
public class CommandHistory {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
private static final int MAX_HISTORY = 50;
public void executeCommand(Command cmd) {
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // 新操作清空重做栈
if (undoStack.size() > MAX_HISTORY) {
undoStack.remove(0); // 移除最旧的
}
}
public void undo() {
if (undoStack.isEmpty()) return;
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
public void redo() {
if (redoStack.isEmpty()) return;
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
嗯,这里有个坑:撤销和重做必须成对出现。每次执行新命令时,一定要清空重做栈。否则用户会看到「重做」了一个已经被覆盖的操作,那逻辑就乱了。
实战:事务回滚
事务回滚是命令模式最经典的应用之一。我在做支付系统时就用过这个思路。
假设一个转账操作:需要扣减账户A的余额,增加账户B的余额。如果第二步失败了,第一步必须回滚。
// 数据库操作命令
public class TransferCommand implements Command {
private Account fromAccount;
private Account toAccount;
private double amount;
private boolean executed = false;
public TransferCommand(Account from, Account to, double amount) {
this.fromAccount = from;
this.toAccount = to;
this.amount = amount;
}
@Override
public void execute() {
if (executed) return;
fromAccount.debit(amount); // 扣款
toAccount.credit(amount); // 加款
executed = true;
}
@Override
public void undo() {
if (!executed) return;
toAccount.debit(amount); // 回滚:从B扣回
fromAccount.credit(amount); // 回滚:给A加回
executed = false;
}
}
// 事务管理器
public class TransactionManager {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command cmd) {
commands.add(cmd);
}
public void executeAll() {
List<Command> executed = new ArrayList<>();
try {
for (Command cmd : commands) {
cmd.execute();
executed.add(cmd);
}
} catch (Exception e) {
// 回滚所有已执行的命令
System.out.println("事务失败,开始回滚...");
Collections.reverse(executed);
for (Command cmd : executed) {
cmd.undo();
}
throw new RuntimeException("事务回滚完成", e);
}
}
}
命令模式的高级用法:宏命令
宏命令就是一组命令的组合。你可以把多个命令打包成一个命令来执行。
public class MacroCommand implements Command {
private List<Command> commands;
public MacroCommand(List<Command> commands) {
this.commands = commands;
}
@Override
public void execute() {
for (Command cmd : commands) {
cmd.execute();
}
}
@Override
public void undo() {
// 逆序撤销
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
宏命令在批量操作中特别有用。比如用户选中了10个文件,然后点击「删除」。这10个删除操作可以打包成一个宏命令,撤销时一次性恢复。
命令模式的优缺点
| 优点 | 缺点 |
|---|---|
| 解耦调用者和接收者 | 类数量会增多(每个操作一个类) |
| 支持撤销/重做 | 命令对象可能占用较多内存 |
| 支持命令排队和日志 | 调试时不太直观(调用链变长) |
| 可以组合成宏命令 | 需要额外管理命令生命周期 |
什么时候用命令模式?
我个人习惯在以下场景使用命令模式:
- 需要支持撤销/重做功能(编辑器、绘图工具)
- 需要将请求排队或记录日志(任务调度、审计)
- 需要支持事务回滚(数据库操作、支付系统)
- 需要将请求参数化(菜单系统、快捷键绑定)
说白了,只要你的系统里「操作」本身需要被当作对象来管理,命令模式就是最佳选择。