设计模式(十八)命令模式 —— 将操作封装成对象,实现撤销、队列等扩展

"请求即对象,行为可编程。"

在现代软件系统中,我们常常需要支持撤销/重做 、将操作排队执行 、实现事务回滚 ,或解耦调用者与执行者。若直接通过方法调用或硬编码逻辑,这些需求将导致代码高度耦合、难以扩展、无法动态组合。

命令模式(Command Pattern) 的核心思想是:将一个请求(操作)封装为一个独立的对象 ,使其具备可存储、可传递、可撤销、可组合的能力。

本文将从原理到实践,深入剖析命令模式在 Java 中的工业级应用。


🧠 一、命令模式的本质:请求的对象化

命令模式源于对"行为"的抽象。传统面向对象编程中,行为通常依附于对象的方法;而命令模式则将行为本身提升为对象------即"动作"可以被创建、传递、保存甚至撤销。

这种设计打破了"调用者必须知道接收者及其方法"的限制,使系统结构更灵活、模块间耦合更低。本质上,命令模式实现了控制反转(Inversion of Control):调用者不再直接控制执行逻辑,而是委托给命令对象。

其哲学基础是:行为也是一种状态。当我们将"做什么"和"怎么做"分离,并把前者封装为对象,就获得了前所未有的组合能力。


🔗 二、核心角色与职责划分

命令模式包含四个关键角色,它们共同构成一个松耦合的请求处理机制:

  • Command(命令接口) :声明执行方法(如 execute()),定义命令的统一契约。
  • ConcreteCommand(具体命令) :实现 Command 接口,持有对 Receiver 的引用,并在 execute() 中调用 Receiver 的具体方法。
  • Receiver(接收者):真正执行业务逻辑的对象,对命令模式"无感"------它不知道自己被哪个命令调用。
  • Invoker(调用者):持有命令对象,负责触发命令执行,但不关心命令的具体内容。

这种结构使得 Invoker 与 Receiver 完全解耦。例如,一个按钮(Invoker)只需持有一个 Command,无需知道背后是保存文件、发送邮件还是格式化文本。
uses holds Command +execute() ConcreteCommand -receiver: Receiver +execute() Receiver +action() Invoker -command: Command +click()


⚙️ 三、基础实现:从方法调用到命令对象

考虑一个简单的文本编辑器场景:用户点击"加粗"按钮,文本变为粗体。若采用传统方式:

java 复制代码
boldButton.addActionListener(e -> editor.setBold(true));

此代码将 UI 与业务逻辑紧耦合。一旦需求变化(如支持撤销),修改成本极高。

使用命令模式重构:

java 复制代码
public interface Command {
    void execute();
}

public class BoldCommand implements Command {
    private final TextEditor editor;

    public BoldCommand(TextEditor editor) {
        this.editor = editor;
    }

    @Override
    public void execute() {
        editor.setBold(true);
    }
}

// 调用者
public class ToolbarButton {
    private Command command;

    public void setCommand(Command cmd) {
        this.command = cmd;
    }

    public void click() {
        command.execute();
    }
}

此时,按钮只依赖 Command 接口,可轻松替换为 ItalicCommandSaveCommand 等,实现高内聚低耦合。


↪️ 四、支持撤销:引入 Undo 机制

命令模式的强大之处在于天然支持撤销。只需在 Command 中增加 undo() 方法,并由具体命令保存执行前的状态。

为此,可定义扩展接口:

java 复制代码
public interface UndoableCommand extends Command {
    void undo();
}

以加粗命令为例:

java 复制代码
public class BoldCommand implements UndoableCommand {
    private final TextEditor editor;
    private final boolean wasBold; // 执行前状态快照

    public BoldCommand(TextEditor editor) {
        this.editor = editor;
        this.wasBold = editor.isBold();
    }

    @Override
    public void execute() {
        editor.setBold(true);
    }

    @Override
    public void undo() {
        editor.setBold(wasBold); // 恢复原状
    }
}

💡 关键原则:撤销不是"反向操作",而是"状态回滚"。因此必须在执行前捕获上下文。


📜 五、命令历史管理:Undo/Redo 栈

为了支持连续撤销与重做,需引入命令历史管理器:

java 复制代码
public class CommandHistory {
    private final Deque<UndoableCommand> undoStack = new ArrayDeque<>();
    private final Deque<UndoableCommand> redoStack = new ArrayDeque<>();

    public void execute(UndoableCommand cmd) {
        cmd.execute();
        undoStack.push(cmd);
        redoStack.clear(); // 新操作使重做栈失效
    }

    public void undo() {
        if (!undoStack.isEmpty()) {
            UndoableCommand cmd = undoStack.pop();
            cmd.undo();
            redoStack.push(cmd);
        }
    }

    public void redo() {
        if (!redoStack.isEmpty()) {
            UndoableCommand cmd = redoStack.pop();
            cmd.execute();
            undoStack.push(cmd);
        }
    }
}

该设计确保了操作的线性历史,且重做仅在撤销后有效,符合用户直觉。


📦 六、宏命令:批量操作的组合

宏命令(Macro Command)允许将多个命令组合为一个逻辑单元,实现"一键执行多步"。

java 复制代码
public class MacroCommand implements UndoableCommand {
    private final List<UndoableCommand> commands = new ArrayList<>();

    public void add(UndoableCommand cmd) {
        commands.add(cmd);
    }

    @Override
    public void execute() {
        commands.forEach(UndoableCommand::execute);
    }

    @Override
    public void undo() {
        // 撤销必须逆序
        for (int i = commands.size() - 1; i >= 0; i--) {
            commands.get(i).undo();
        }
    }
}

例如,用户可录制一个"格式化段落"宏:加粗 + 斜体 + 居中。执行一次即完成全部操作,撤销一次也全部回退。


⏳ 七、异步命令:非阻塞执行模型

在 Java 中,可通过 CompletableFuture 实现异步命令,适用于 I/O 密集型任务(如文件保存、网络请求):

java 复制代码
public abstract class AsyncCommand implements Command {
    protected CompletableFuture<Void> future = CompletableFuture.completedFuture(null);

    @Override
    public final void execute() {
        this.future = CompletableFuture.runAsync(this::doExecute);
    }

    protected abstract void doExecute();

    public CompletableFuture<Void> getFuture() {
        return future;
    }
}

使用时:

java 复制代码
AsyncCommand saveCmd = new AsyncCommand() {
    @Override
    protected void doExecute() {
        fileService.save(document);
    }
};
saveCmd.execute();
saveCmd.getFuture().thenRun(() -> ui.showMessage("Saved!"));

⚠️ 注意:异步命令的撤销需额外设计(如版本控制、幂等删除),避免竞态条件。


🖥️ 八、GUI 应用实战:可撤销的编辑器

在 Swing 或 JavaFX 中,命令模式是构建可撤销 UI 的标准方案。以 JTextArea 为例:

java 复制代码
class ReplaceTextCommand implements UndoableCommand {
    private final JTextArea area;
    private final String oldText, newText;
    private final int start, end;

    public ReplaceTextCommand(JTextArea a, String nt) {
        this.area = a;
        this.newText = nt;
        this.start = a.getSelectionStart();
        this.end = a.getSelectionEnd();
        this.oldText = a.getText().substring(start, end);
    }

    @Override
    public void execute() {
        area.replaceRange(newText, start, end);
    }

    @Override
    public void undo() {
        area.replaceRange(oldText, start, start + newText.length());
    }
}

用户选中文本 → 点击"加粗" → 自动包裹 <b> 标签 → 支持完整 Undo/Redo。整个过程无需 UI 层了解文本处理细节。


🔐 九、安全与审计:命令的增强包装

命令对象可作为横切关注点的载体。例如,在执行敏感操作前进行权限校验:

java 复制代码
public class SecureCommand implements UndoableCommand {
    private final UndoableCommand delegate;
    private final User user;
    private final String requiredRole;

    public SecureCommand(UndoableCommand cmd, User u, String role) {
        this.delegate = cmd;
        this.user = u;
        this.requiredRole = role;
    }

    @Override
    public void execute() {
        if (!user.hasRole(requiredRole)) {
            throw new SecurityException("Access denied");
        }
        AuditLog.record(user.getId(), delegate.getClass().getSimpleName());
        delegate.execute();
    }

    @Override
    public void undo() {
        delegate.undo();
    }
}

这种装饰器风格的包装,使安全与审计逻辑与业务命令正交,符合开闭原则。


📊 十、性能考量与适用边界

命令模式并非万能。其优势在以下场景显著:

  • 需要操作历史(Undo/Redo)
  • 需要任务队列或调度
  • 需要将请求参数化(如菜单项绑定不同命令)
  • 需要事务性操作(全部成功或全部回滚)

但在以下情况应谨慎使用:

  • 高频调用路径:如游戏主循环中的每帧更新,命令对象的创建开销可能成为瓶颈。
  • 简单回调:若无需撤销、队列或日志,直接使用 Lambda 表达式更简洁。
  • 内存敏感环境:命令对象若持有大对象引用(如整个文档),可能导致内存泄漏。

✅ 建议:对高频路径使用函数式接口(如 Runnable),对复杂交互使用命令模式。


🧪 十一、可测试性:命令天然适合单元测试

由于命令封装了完整的行为上下文,其单元测试极为直观:

java 复制代码
@Test
void boldCommand_undo_restores_original_state() {
    TextEditor editor = new TextEditor();
    editor.setBold(false);
    
    BoldCommand cmd = new BoldCommand(editor);
    cmd.execute();
    assertTrue(editor.isBold());
    
    cmd.undo();
    assertFalse(editor.isBold());
}

每个命令都是一个独立的测试单元,无需启动 GUI 或模拟复杂环境。


🌐 十二、与 Spring 等框架集成

在 Spring 应用中,命令可作为 Prototype Bean 动态注入:

java 复制代码
@Component
@Scope("prototype")
public class SendEmailCommand implements UndoableCommand {
    private final EmailService service;
    private final String to;
    private boolean sent = false;

    public SendEmailCommand(EmailService s, String recipient) {
        this.service = s;
        this.to = recipient;
    }

    @Override
    public void execute() {
        service.send(to, "Hello");
        sent = true;
    }

    @Override
    public void undo() {
        if (sent) service.recall(to); // 假设有撤回 API
    }
}

通过 ApplicationContext.getBean() 获取新实例,确保每次操作独立,避免状态污染。


🛑 十三、常见反模式与陷阱

  1. 过度封装:仅为打印日志创建命令,违背 YAGNI 原则。
  2. 状态快照不完整:撤销失败常因未保存足够上下文(如忽略光标位置)。
  3. 异步撤销竞态:多次快速操作后撤销,可能误删最新数据。
  4. 内存泄漏:命令长期持有大对象(如图像、文档),执行后未释放。

✅ 最佳实践:命令执行后主动置空大引用;撤销逻辑必须经过充分测试。


🎯 十四、结语:命令是行为的容器

命令模式远不止于"封装方法调用"。它是将行为提升为一等公民的设计哲学:

  • 可存储 → 实现操作历史;
  • 可传递 → 解耦调用者与执行者;
  • 可组合 → 构建复杂工作流;
  • 可撤销 → 提升用户体验;
  • 可审计 → 满足合规要求。

无论是在桌面应用、Web 后端,还是自动化测试框架中,命令模式都持续发挥着不可替代的作用。

下次当你需要"记住用户做了什么,并能回退"时,请记住:
不是你在调用方法,而是在发出命令。`;

相关推荐
settingsun12254 小时前
AI App: Tool Use Design Pattern 工具使用设计模式
设计模式
y***548817 小时前
PHP框架设计模式
设计模式
口袋物联20 小时前
设计模式之适配器模式在 C 语言中的应用(含 Linux 内核实例)
c语言·设计模式·适配器模式
MobotStone20 小时前
大数据:我们是否在犯一个大错误?
设计模式·架构
7***n751 天前
前端设计模式详解
前端·设计模式·状态模式
兵bing1 天前
设计模式-装饰器模式
设计模式·装饰器模式
雨中飘荡的记忆1 天前
深入理解设计模式之适配器模式
java·设计模式
雨中飘荡的记忆1 天前
深入理解设计模式之装饰者模式
java·设计模式
老鼠只爱大米1 天前
Java设计模式之外观模式(Facade)详解
java·设计模式·外观模式·facade·java设计模式