命令模式通过将请求封装为独立的命令对象,解耦了请求发起者和请求执行者,而支持撤销操作的命令模式框架则是在此基础上增加了操作历史记录与反向执行能力,让系统可以回溯之前的操作状态。这种设计在文本编辑器、图形绘制工具、表单操作等场景中非常实用,用户可以通过撤销、重做操作修正误操作,提升使用体验。

核心角色定义
一个支持撤销操作的命令模式框架需要包含以下几个核心角色,每个角色各司其职,共同支撑框架的完整功能:
- 命令接口(Command):定义所有命令的统一执行与撤销方法,是框架的基础契约。
- 具体命令(ConcreteCommand):实现命令接口,绑定接收者对象,封装具体的执行逻辑与撤销逻辑。
- 接收者(Receiver):真正执行命令逻辑的对象,包含具体的业务操作方法。
- 调用者(Invoker):负责触发命令的执行,同时维护操作历史栈,管理撤销与重做操作。
- 客户端(Client):创建具体命令对象,设置命令的接收者,将命令交给调用者执行。
基础接口设计
首先定义命令接口,所有具体命令都需要实现该接口的两个核心方法:execute 用于执行命令,undo 用于撤销命令。
// 命令接口定义
public interface Command {
// 执行命令
void execute();
// 撤销命令
void undo();
}
接下来定义接收者角色,以文本编辑器为例,接收者负责维护文本内容,提供文本追加和删除的方法:
// 文本编辑接收者
public class TextEditor {
private StringBuilder content = new StringBuilder();
// 追加文本
public void appendText(String text) {
content.append(text);
}
// 删除末尾指定长度的文本
public void deleteText(int length) {
if (length <= content.length()) {
content.delete(content.length() - length, content.length());
}
}
// 获取当前文本内容
public String getContent() {
return content.toString();
}
}
具体命令实现
具体命令需要绑定对应的接收者,实现执行和撤销的具体逻辑。以文本追加命令为例,执行时调用接收者的追加方法,撤销时调用接收者的删除方法,删除长度就是本次追加的文本长度。
// 文本追加命令
public class AppendTextCommand implements Command {
private TextEditor editor;
private String appendedText;
public AppendTextCommand(TextEditor editor, String appendedText) {
this.editor = editor;
this.appendedText = appendedText;
}
@Override
public void execute() {
editor.appendText(appendedText);
}
@Override
public void undo() {
editor.deleteText(appendedText.length());
}
}
调用者与历史管理
调用者需要维护两个栈:一个用于存放已执行命令的撤销栈,一个用于存放已撤销命令的重做栈。执行命令时将命令压入撤销栈,清空重做栈;撤销命令时从撤销栈弹出命令执行撤销,同时压入重做栈;重做命令时从重做栈弹出命令执行,同时压入撤销栈。
import java.util.Stack;
// 命令调用者,管理操作历史
public class CommandInvoker {
// 撤销栈,存放已执行的命令
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);
}
}
// 重做操作
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
undoStack.push(command);
}
}
// 判断是否可以撤销
public boolean canUndo() {
return !undoStack.isEmpty();
}
// 判断是否可以重做
public boolean canRedo() {
return !redoStack.isEmpty();
}
}
框架使用示例
客户端只需要创建接收者、具体命令,将命令交给调用者执行即可,无需关心命令的具体执行细节和历史管理逻辑。
public class Client {
public static void main(String[] args) {
// 创建接收者
TextEditor editor = new TextEditor();
// 创建调用者
CommandInvoker invoker = new CommandInvoker();
// 执行追加命令
Command append1 = new AppendTextCommand(editor, "Hello ");
invoker.executeCommand(append1);
System.out.println("第一次追加后内容:" + editor.getContent());
Command append2 = new AppendTextCommand(editor, "World");
invoker.executeCommand(append2);
System.out.println("第二次追加后内容:" + editor.getContent());
// 撤销操作
if (invoker.canUndo()) {
invoker.undo();
System.out.println("第一次撤销后内容:" + editor.getContent());
}
// 重做操作
if (invoker.canRedo()) {
invoker.redo();
System.out.println("第一次重做后内容:" + editor.getContent());
}
// 再次撤销
if (invoker.canUndo()) {
invoker.undo();
System.out.println("第二次撤销后内容:" + editor.getContent());
}
}
}
框架扩展方向
上述基础框架可以根据实际需求进行扩展:
- 增加命令的合并能力,比如连续的文本输入可以合并为一个命令,减少撤销栈的存储压力。
- 支持命令的持久化,将操作历史保存到本地文件或数据库,实现跨会话的撤销重做。
- 增加命令的执行状态校验,避免重复执行已完成的命令。
- 支持批量命令,将多个小命令封装为一个组合命令,执行和撤销时批量处理。
设计支持撤销操作的命令模式框架时,核心是保证每个命令的撤销逻辑与执行逻辑完全可逆,同时历史管理模块要清晰维护操作顺序,避免出现状态不一致的问题。