命令模式:操作撤销与重做
大家好,我是老张。今天我们来聊聊命令模式。
说实话,这个模式是我在开发中最常用的几个模式之一。为什么?因为几乎每个需要用户交互的系统,都离不开撤销和重做。你想想看,文本编辑器里的Ctrl+Z、Ctrl+Y,任务队列里的任务调度,甚至游戏里的回放功能——背后都是命令模式在撑腰。
命令模式是什么?
说白了,命令模式就是把一个请求封装成一个对象。这样你就可以把请求排队、记录日志、支持撤销操作。
我个人的理解是:把「做什么」和「怎么做」分开。调用者只需要告诉命令对象「我要做这件事」,至于具体怎么执行,那是命令对象自己的事。
核心定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。
模式结构
命令模式有四个角色,我画了张图帮你理清关系:
实战:文本编辑器的撤销与重做
我记得有一次给一个在线文档系统做架构设计,用户要求支持无限次撤销和重做。当时我就想到了命令模式。
先看代码,我直接给你一个精简但完整的实现:
// 1. 命令接口
public interface Command {
void execute();
void undo();
}
// 2. 接收者——文本编辑器
public class TextEditor {
private StringBuilder content = new StringBuilder();
public void insert(int position, String text) {
content.insert(position, text);
}
public void delete(int start, int end) {
content.delete(start, end);
}
public String getContent() {
return content.toString();
}
}
// 3. 具体命令——插入操作
public class InsertCommand implements Command {
private TextEditor editor;
private String text;
private int position;
public InsertCommand(TextEditor editor, int position, String text) {
this.editor = editor;
this.position = position;
this.text = text;
}
@Override
public void execute() {
editor.insert(position, text);
}
@Override
public void undo() {
editor.delete(position, position + text.length());
}
}
// 4. 具体命令——删除操作
public class DeleteCommand implements Command {
private TextEditor editor;
private int start;
private int end;
private String deletedText; // 保存被删内容,用于撤销
public DeleteCommand(TextEditor editor, int start, int end) {
this.editor = editor;
this.start = start;
this.end = end;
}
@Override
public void execute() {
deletedText = editor.getContent().substring(start, end);
editor.delete(start, end);
}
@Override
public void undo() {
editor.insert(start, deletedText);
}
}
// 5. 调用者——管理撤销/重做
public class CommandManager {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
public void executeCommand(Command cmd) {
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // 新操作清空重做栈
}
public void undo() {
if (!undoStack.isEmpty()) {
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
}
我的经验:保存被删除的文本内容很重要。如果不保存,撤销时你根本不知道删了什么。这个坑我曾经踩过,后来就学乖了。
任务队列与线程池中的应用
命令模式在任务调度场景下也特别好用。你想想看,线程池里的每个任务,本质上就是一个命令对象。
我参与过一个电商系统的订单处理模块,高峰期每秒要处理上千个订单。我们用命令模式把每个订单封装成任务,丢到线程池里执行。这样既实现了异步处理,又能轻松控制并发数。
// 任务命令接口
public interface TaskCommand extends Runnable {
void execute();
void rollback(); // 任务回滚
}
// 具体任务——发送邮件
public class SendEmailTask implements TaskCommand {
private String to;
private String content;
public SendEmailTask(String to, String content) {
this.to = to;
this.content = content;
}
@Override
public void execute() {
// 发送邮件逻辑
System.out.println("发送邮件给:" + to);
}
@Override
public void rollback() {
// 邮件发送失败后的补偿操作
System.out.println("邮件发送失败,记录日志");
}
@Override
public void run() {
execute();
}
}
// 任务队列管理器
public class TaskQueue {
private BlockingQueue<TaskCommand> queue = new LinkedBlockingQueue<>();
private ExecutorService executor = Executors.newFixedThreadPool(10);
public void addTask(TaskCommand task) {
queue.offer(task);
executor.submit(task);
}
}
命令模式的优缺点
| 优点 | 缺点 |
|---|---|
| 将请求发送者和接收者解耦 | 可能导致类数量膨胀 |
| 支持撤销和重做操作 | 每个命令都是一个类,增加维护成本 |
| 支持命令的排队和日志记录 | 命令对象可能占用较多内存 |
| 可以轻松实现宏命令(组合命令) | 需要额外管理命令的生命周期 |
避坑指南:我曾经在一个项目中,把每个鼠标点击都封装成命令对象。结果内存暴涨,因为用户操作太频繁了。后来我加了限制——只保留最近100步的操作历史。记住,无限撤销是有代价的。
什么时候用命令模式?
我个人总结了三个典型场景:
- 需要撤销操作:文本编辑器、绘图软件、数据库事务
- 需要任务排队:线程池、消息队列、批处理系统
- 需要记录操作日志:审计系统、操作回放、宏录制
说白了,只要你的系统里有「操作」这个概念,并且这些操作需要被管理、被记录、被回放,那命令模式就是你的不二之选。
嗯,今天就聊到这里。命令模式其实不难,关键是理解它的核心思想——把请求变成对象。一旦你掌握了这个思路,很多复杂的问题都会变得简单起来。