一文搞懂命令模式,撤销重做的终极秘籍

一文搞懂命令模式,撤销重做的终极秘籍

🎯 为什么你需要命令模式?

想象一下,你正在用Word写文档,突然手滑删了一大段精心编写的内容。这时候,你会怎么做?当然是疯狂按Ctrl+Z撤销啊!

但你有没有想过,这个看似简单的"撤销"功能,背后的实现可没那么简单。今天,我们就来聊聊设计模式中的"瑞士军刀"——命令模式(Command Pattern),它正是实现撤销/重做功能的最佳选择!

📚 命令模式到底是个啥?

通俗理解

命令模式就像是给你的代码配了个万能遥控器。想想家里的电视遥控器:

  • 你按下"开机"按钮 → 电视开了
  • 你按下"换台"按钮 → 频道切换了
  • 你按下"音量+"按钮 → 声音变大了

遥控器不需要知道电视内部是怎么工作的,它只负责发送命令。这就是命令模式的精髓:将请求封装成对象,让你可以用不同的请求参数化客户端

正经定义

命令模式将一个请求封装为一个对象,从而让你可以:

  • 参数化不同的请求
  • 将请求排队或记录请求日志
  • 支持可撤销的操作

🔧 命令模式能解决什么问题?

1. 撤销/重做功能

这是命令模式的经典应用场景。每个操作都被封装成命令对象,可以轻松实现撤销和重做。

2. 任务队列

将命令对象放入队列,可以实现异步执行、延迟执行等功能。

3. 宏命令

将多个命令组合成一个大命令,一键执行多个操作。就像游戏中的"一键换装"。

4. 日志和审计

每个命令都是对象,可以序列化保存,方便记录操作历史。

💡 命令模式的核心实现

让我们通过一个简单的文本编辑器例子,来看看命令模式是如何工作的。

核心角色

命令模式包含以下几个角色:

  1. Command(命令接口):定义执行操作的接口
  2. ConcreteCommand(具体命令):实现命令接口,执行具体操作
  3. Receiver(接收者):真正执行操作的对象
  4. Invoker(调用者):负责调用命令对象
  5. 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: 命令模式的优缺点是什么?

优点:

  1. 解耦了请求的发送者和接收者
  2. 可以轻松实现撤销和重做
  3. 可以将命令组合成复合命令
  4. 新增命令很方便,符合开闭原则

缺点:

  1. 可能会导致系统中有过多的具体命令类
  2. 增加了系统的复杂度

Q4: 在Spring框架中有哪些命令模式的应用?

  1. Spring MVC的HandlerAdapter:将不同类型的Handler适配成统一的处理方式
  2. JdbcTemplate的回调接口:如RowMapper、PreparedStatementSetter等
  3. Spring Batch的Job和Step:将批处理任务封装成可执行的命令

🎁 总结:命令模式的精髓

命令模式就像是给你的代码装上了"时光机",让操作可以被记录、撤销、重做。记住这几个要点:

  1. 封装请求:将操作封装成对象,让请求参数化
  2. 解耦发送者和接收者:通过命令对象作为中介
  3. 支持撤销:命令对象保存状态,可以回滚操作
  4. 易于扩展:新增命令只需要实现接口即可

下次当你需要实现撤销/重做功能,或者需要将操作排队执行时,别忘了命令模式这个好帮手!

🤔 思考题

试着用命令模式实现一个简单的画图程序,支持:

  1. 画直线、画圆、画矩形
  2. 撤销/重做功能
  3. 保存绘图步骤,下次打开可以回放

欢迎在评论区分享你的实现思路!


作者说:如果这篇文章对你有帮助,别忘了点赞收藏哦!有任何问题欢迎在评论区讨论,我会及时回复的~ 😊

运行结果

=== 开始编辑文档 ===

添加文本: 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. 宏命令 - 一键执行多个操作

想象一下,你要实现一个"一键格式化"功能,需要:

  1. 删除所有空格
  2. 首字母大写
  3. 添加句号

这时候就可以用宏命令:

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();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慢德

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值