一文搞懂命令模式,撤销重做的终极秘籍
🎯 为什么你需要命令模式?
想象一下,你正在用Word写文档,突然手滑删了一大段精心编写的内容。这时候,你会怎么做?当然是疯狂按Ctrl+Z撤销啊!
但你有没有想过,这个看似简单的"撤销"功能,背后的实现可没那么简单。今天,我们就来聊聊设计模式中的"瑞士军刀"——命令模式(Command Pattern),它正是实现撤销/重做功能的最佳选择!
📚 命令模式到底是个啥?
通俗理解
命令模式就像是给你的代码配了个万能遥控器。想想家里的电视遥控器:
- 你按下"开机"按钮 → 电视开了
- 你按下"换台"按钮 → 频道切换了
- 你按下"音量+"按钮 → 声音变大了
遥控器不需要知道电视内部是怎么工作的,它只负责发送命令。这就是命令模式的精髓:将请求封装成对象,让你可以用不同的请求参数化客户端。
正经定义
命令模式将一个请求封装为一个对象,从而让你可以:
- 参数化不同的请求
- 将请求排队或记录请求日志
- 支持可撤销的操作
🔧 命令模式能解决什么问题?
1. 撤销/重做功能
这是命令模式的经典应用场景。每个操作都被封装成命令对象,可以轻松实现撤销和重做。
2. 任务队列
将命令对象放入队列,可以实现异步执行、延迟执行等功能。
3. 宏命令
将多个命令组合成一个大命令,一键执行多个操作。就像游戏中的"一键换装"。
4. 日志和审计
每个命令都是对象,可以序列化保存,方便记录操作历史。
💡 命令模式的核心实现
让我们通过一个简单的文本编辑器例子,来看看命令模式是如何工作的。
核心角色
命令模式包含以下几个角色:
- Command(命令接口):定义执行操作的接口
- ConcreteCommand(具体命令):实现命令接口,执行具体操作
- Receiver(接收者):真正执行操作的对象
- Invoker(调用者):负责调用命令对象
- Client(客户端):创建命令对象并设置接收者
代码实现
下面我们来实现一个简单的文本编辑器,支持添加文本、删除文本,以及撤销/重做功能。
// 1. 命令接口
public interface Command {
void execute(); // 执行命令
void undo(); // 撤销命令
}
// 2. 接收者 - 文本编辑器
public class TextEditor {
private StringBuilder content = new StringBuilder();
public void addText(String text) {
content.append(text);
System.out.println("添加文本: " + text);
System.out.println("当前内容: " + content);
}
public void deleteText(int length) {
if (length > content.length()) {
length = content.length();
}
content.delete(content.length() - length, content.length());
System.out.println("删除了 " + length + " 个字符");
System.out.println("当前内容: " + content);
}
public String getContent() {
return content.toString();
}
}
// 3. 具体命令 - 添加文本命令
public class AddTextCommand implements Command {
private TextEditor editor;
private String text;
public AddTextCommand(TextEditor editor, String text) {
this.editor = editor;
this.text = text;
}
@Override
public void execute() {
editor.addText(text);
}
@Override
public void undo() {
// 撤销添加就是删除相应长度的文本
editor.deleteText(text.length());
}
}
// 4. 具体命令 - 删除文本命令
public class DeleteTextCommand implements Command {
private TextEditor editor;
private int length;
private String deletedText; // 保存被删除的文本,用于撤销
public DeleteTextCommand(TextEditor editor, int length) {
this.editor = editor;
this.length = length;
}
@Override
public void execute() {
String content = editor.getContent();
if (length > content.length()) {
length = content.length();
}
// 保存要删除的文本
deletedText = content.substring(content.length() - length);
editor.deleteText(length);
}
@Override
public void undo() {
// 撤销删除就是把删除的文本加回去
if (deletedText != null) {
editor.addText(deletedText);
}
}
}
// 5. 调用者 - 命令管理器
public class CommandManager {
private Stack<Command> undoStack = new Stack<>();
private Stack<Command> redoStack = new Stack<>();
public void executeCommand(Command command) {
command.execute();
undoStack.push(command);
// 执行新命令后,清空重做栈
redoStack.clear();
}
public void undo() {
if (!undoStack.isEmpty()) {
Command command = undoStack.pop();
command.undo();
redoStack.push(command);
System.out.println("撤销操作完成");
} else {
System.out.println("没有可撤销的操作");
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
System.out.println("重做操作完成");
} else {
System.out.println("没有可重做的操作");
}
}
}
// 6. 客户端使用示例
public class Client {
public static void main(String[] args) {
// 创建接收者
TextEditor editor = new TextEditor();
// 创建命令管理器
CommandManager manager = new CommandManager();
System.out.println("=== 开始编辑文档 ===\n");
// 执行一系列命令
manager.executeCommand(new AddTextCommand(editor, "Hello "));
manager.executeCommand(new AddTextCommand(editor, "World!"));
manager.executeCommand(new AddTextCommand(editor, " Java"));
System.out.println("\n--- 删除操作 ---");
manager.executeCommand(new DeleteTextCommand(editor, 5)); // 删除" Java"
System.out.println("\n--- 撤销操作 ---");
manager.undo(); // 恢复" Java"
System.out.println("\n--- 再次撤销 ---");
manager.undo(); // 删除" Java"
manager.undo(); // 删除"World!"
System.out.println("\n--- 重做操作 ---");
manager.redo(); // 恢复"World!"
System.out.println("\n最终内容: " + editor.getContent());
}
}
2. 命令队列 - 异步执行
在某些场景下,你可能需要将命令放入队列中异步执行:
public class CommandQueue {
private Queue<Command> queue = new LinkedList<>();
private boolean isProcessing = false;
public void addCommand(Command command) {
queue.offer(command);
processCommands();
}
private void processCommands() {
if (!isProcessing && !queue.isEmpty()) {
isProcessing = true;
// 模拟异步执行
new Thread(() -> {
while (!queue.isEmpty()) {
Command command = queue.poll();
command.execute();
try {
Thread.sleep(1000); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isProcessing = false;
}).start();
}
}
}
3. 命令日志 - 持久化存储
将命令序列化后保存,可以实现操作日志和回放功能:
public interface SerializableCommand extends Command, Serializable {
String toJson();
void fromJson(String json);
}
💬 面试热点:命令模式常见问题
Q1: 命令模式和策略模式有什么区别?
答案要点:
- 目的不同:命令模式关注的是"做什么"(封装请求),策略模式关注的是"怎么做"(封装算法)
- 撤销支持:命令模式天生支持撤销操作,策略模式不支持
- 接收者:命令模式通常有明确的接收者,策略模式没有
- 使用场景:命令模式适合需要撤销、队列、日志的场景;策略模式适合算法替换的场景
Q2: 如何实现有限次数的撤销?
public class LimitedCommandManager {
private static final int MAX_UNDO_SIZE = 10;
private LinkedList<Command> undoList = new LinkedList<>();
public void executeCommand(Command command) {
command.execute();
if (undoList.size() >= MAX_UNDO_SIZE) {
undoList.removeFirst(); // 移除最老的命令
}
undoList.addLast(command);
}
}
Q3: 命令模式的优缺点是什么?
优点:
- 解耦了请求的发送者和接收者
- 可以轻松实现撤销和重做
- 可以将命令组合成复合命令
- 新增命令很方便,符合开闭原则
缺点:
- 可能会导致系统中有过多的具体命令类
- 增加了系统的复杂度
Q4: 在Spring框架中有哪些命令模式的应用?
- Spring MVC的HandlerAdapter:将不同类型的Handler适配成统一的处理方式
- JdbcTemplate的回调接口:如RowMapper、PreparedStatementSetter等
- Spring Batch的Job和Step:将批处理任务封装成可执行的命令
🎁 总结:命令模式的精髓
命令模式就像是给你的代码装上了"时光机",让操作可以被记录、撤销、重做。记住这几个要点:
- 封装请求:将操作封装成对象,让请求参数化
- 解耦发送者和接收者:通过命令对象作为中介
- 支持撤销:命令对象保存状态,可以回滚操作
- 易于扩展:新增命令只需要实现接口即可
下次当你需要实现撤销/重做功能,或者需要将操作排队执行时,别忘了命令模式这个好帮手!
🤔 思考题
试着用命令模式实现一个简单的画图程序,支持:
- 画直线、画圆、画矩形
- 撤销/重做功能
- 保存绘图步骤,下次打开可以回放
欢迎在评论区分享你的实现思路!
作者说:如果这篇文章对你有帮助,别忘了点赞收藏哦!有任何问题欢迎在评论区讨论,我会及时回复的~ 😊
运行结果
=== 开始编辑文档 ===
添加文本: Hello
当前内容: Hello
添加文本: World!
当前内容: Hello World!
添加文本: Java
当前内容: Hello World! Java
--- 删除操作 ---
删除了 5 个字符
当前内容: Hello World!
--- 撤销操作 ---
添加文本: Java
当前内容: Hello World! Java
撤销操作完成
--- 再次撤销 ---
删除了 5 个字符
当前内容: Hello World!
撤销操作完成
删除了 6 个字符
当前内容: Hello
撤销操作完成
--- 重做操作 ---
添加文本: World!
当前内容: Hello World!
重做操作完成
最终内容: Hello World!
🚀 扩展:命令模式的进阶玩法
1. 宏命令 - 一键执行多个操作
想象一下,你要实现一个"一键格式化"功能,需要:
- 删除所有空格
- 首字母大写
- 添加句号
这时候就可以用宏命令:
public class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
public void addCommand(Command command) {
commands.add(command);
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
// 注意:撤销时要反向执行
for (int i = commands.size() - 1; i >= 0; i--) {
commands.get(i).undo();
}
}
}
2913

被折叠的 条评论
为什么被折叠?



