命令模式:操作撤销与重做

大家好,我是老张。今天我们来聊聊命令模式。

说实话,这个模式是我在开发中最常用的几个模式之一。为什么?因为几乎每个需要用户交互的系统,都离不开撤销和重做。你想想看,文本编辑器里的Ctrl+Z、Ctrl+Y,任务队列里的任务调度,甚至游戏里的回放功能——背后都是命令模式在撑腰。

命令模式是什么?

说白了,命令模式就是把一个请求封装成一个对象。这样你就可以把请求排队、记录日志、支持撤销操作。

我个人的理解是:把「做什么」和「怎么做」分开。调用者只需要告诉命令对象「我要做这件事」,至于具体怎么执行,那是命令对象自己的事。

核心定义:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化。

模式结构

命令模式有四个角色,我画了张图帮你理清关系:

命令模式结构图 Invoker(调用者) 持有命令对象 Command(命令接口) execute() / undo() ConcreteCommand 具体命令实现 Receiver(接收者) 实际执行者 调用者持有命令对象 命令对象调用接收者的方法 接收者执行实际业务逻辑 CommandHistory 撤销/重做栈

实战:文本编辑器的撤销与重做

我记得有一次给一个在线文档系统做架构设计,用户要求支持无限次撤销和重做。当时我就想到了命令模式。

先看代码,我直接给你一个精简但完整的实现:

// 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步的操作历史。记住,无限撤销是有代价的。

什么时候用命令模式?

我个人总结了三个典型场景:

  • 需要撤销操作:文本编辑器、绘图软件、数据库事务
  • 需要任务排队:线程池、消息队列、批处理系统
  • 需要记录操作日志:审计系统、操作回放、宏录制

说白了,只要你的系统里有「操作」这个概念,并且这些操作需要被管理、被记录、被回放,那命令模式就是你的不二之选。

嗯,今天就聊到这里。命令模式其实不难,关键是理解它的核心思想——把请求变成对象。一旦你掌握了这个思路,很多复杂的问题都会变得简单起来。


公众号:蓝海资料掘金营,微信deep3321