命令模式:从撤销操作到分布式调度的命令封装实践

命令模式:从撤销操作到分布式调度的命令封装实践

一、模式核心:将请求封装为可操作的 "命令对象"

在文本编辑器中,"撤销" 功能需要记录每一步操作;在分布式系统中,远程调用需要将请求序列化为可传输的对象。这类场景的核心需求是:将 "请求" 与 "执行" 解耦,使请求可被记录、撤销、重试。** 命令模式(Command Pattern)** 通过将请求封装为独立的命令对象,实现发送者与执行者的解耦,核心解决:

  • 请求参数化 :将操作封装为对象(如SaveCommandUndoCommand
  • 操作可追溯:支持日志记录、撤销(Undo)、重做(Redo)等高级功能

核心思想与 UML 类图

命令模式通过 "命令对象" 解耦请求发送者和接收者,形成可扩展的操作链条:

二、核心实现:构建支持撤销的计算器

1. 定义命令接口(封装操作)

java 复制代码
public interface Command {
    void execute();   // 执行命令
    void undo();      // 撤销命令(可选)
}

2. 实现具体命令(绑定接收者与操作逻辑)

加法命令
java 复制代码
public class AddCommand implements Command {
    private Calculator receiver;  // 接收者(实际执行操作的对象)
    private int operand;          // 操作数
    private int previousResult;   // 保存旧结果用于撤销

    public AddCommand(Calculator receiver, int operand) {
        this.receiver = receiver;
        this.operand = operand;
        this.previousResult = receiver.getResult(); // 记录执行前的状态
    }

    @Override
    public void execute() {
        receiver.add(operand);
    }

    @Override
    public void undo() {
        receiver.setResult(previousResult); // 恢复执行前的状态
    }
}
减法命令
java 复制代码
public class SubtractCommand implements Command {
    private Calculator receiver;
    private int operand;
    private int previousResult;

    public SubtractCommand(Calculator receiver, int operand) {
        this.receiver = receiver;
        this.operand = operand;
        this.previousResult = receiver.getResult();
    }

    @Override
    public void execute() {
        receiver.subtract(operand);
    }

    @Override
    public void undo() {
        receiver.setResult(previousResult);
    }
}

3. 接收者(实际执行操作的实体)

java 复制代码
public class Calculator {
    private int result;

    public void add(int num) {
        result += num;
    }

    public void subtract(int num) {
        result -= num;
    }

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }
}

4. 调用者(触发命令执行)

java 复制代码
public class Invoker {
    private Command command;
    private Command undoCommand; // 记录上一条命令用于撤销

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

    public void execute() {
        if (command != null) {
            command.execute();
            undoCommand = command; // 保存当前命令用于撤销
        }
    }

    public void undo() {
        if (undoCommand != null) {
            undoCommand.undo();
        }
    }
}

5. 客户端调用示例(支持撤销的计算)

java 复制代码
public class ClientDemo {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        Invoker invoker = new Invoker();

        // 执行加法命令
        Command addCmd = new AddCommand(calculator, 10);
        invoker.setCommand(addCmd);
        invoker.execute();
        System.out.println("当前结果:" + calculator.getResult()); // 输出:10

        // 执行减法命令
        Command subtractCmd = new SubtractCommand(calculator, 5);
        invoker.setCommand(subtractCmd);
        invoker.execute();
        System.out.println("当前结果:" + calculator.getResult()); // 输出:5

        // 撤销上一步操作(减法)
        invoker.undo();
        System.out.println("撤销后结果:" + calculator.getResult()); // 输出:10
    }
}

三、进阶:构建支持批处理与日志的命令队列

1. 命令队列(支持批量执行与撤销)

java 复制代码
public class CommandQueue {
    private List<Command> commandList = new ArrayList<>();

    public void addCommand(Command cmd) {
        commandList.add(cmd);
    }

    public void executeAll() {
        commandList.forEach(Command::execute);
    }

    public void undoAll() {
        // 逆序撤销(先进后撤销)
        for (int i = commandList.size() - 1; i >= 0; i--) {
            commandList.get(i).undo();
        }
    }
}

// 使用示例:批量转账命令
CommandQueue queue = new CommandQueue();
queue.addCommand(new TransferCommand(bankA, bankB, 1000));
queue.addCommand(new TransferCommand(bankB, bankC, 500));
queue.executeAll(); // 批量执行
queue.undoAll();    // 批量撤销

2. 持久化命令日志(支持故障恢复)

java 复制代码
public class CommandLogger {
    private FileWriter writer;

    public CommandLogger(String logFile) throws IOException {
        writer = new FileWriter(logFile, true);
    }

    public void logCommand(Command cmd) {
        // 序列化命令到日志文件(需命令实现Serializable)
        writer.write(cmd.getClass().getName() + "\n");
        writer.write(cmd.getState() + "\n"); // 假设命令有获取状态的方法
        writer.flush();
    }

    public List<Command> readCommands() throws IOException {
        // 从日志恢复命令(需反序列化)
        List<Command> commands = new ArrayList<>();
        // 省略具体实现...
        return commands;
    }
}

3. 可视化命令流程

sequenceDiagram participant Client participant Invoker participant Command participant Receiver Client->>Invoker: 设置命令(AddCommand) Invoker->>Command: 执行execute() Command->>Receiver: 调用add() Receiver-->>Command: 执行结果 Command-->>Invoker: 完成执行 Invoker-->>Client: 返回结果 Client->>Invoker: 执行undo() Invoker->>Command: 调用undo() Command->>Receiver: 调用undo() Receiver-->>Command: 恢复状态 Command-->>Invoker: 完成撤销 Invoker-->>Client: 返回撤销结果

四、框架与源码中的命令模式实践

1. Spring MVC 的 Command 对象

  • 核心类CommandObject(如表单提交对象)
  • 原理:将 HTTP 请求参数封装为命令对象,解耦控制器与请求处理逻辑
java 复制代码
// 控制器接收命令对象
@PostMapping("/order")
public String processOrder(@ModelAttribute OrderCommand command) {
    orderService.process(command); // 命令对象包含所有处理所需参数
    return "success";
}

2. Struts 2 的 Action 机制

  • Action 接口是典型的命令对象,封装请求处理逻辑
  • 支持execute()(执行)和validate()(校验),通过Result定义后续流程

3. Swing 事件处理(隐式命令模式)

  • 按钮点击事件的ActionListener本质是命令对象
java 复制代码
JButton saveButton = new JButton("保存");
saveButton.addActionListener(e -> {
    // 命令的execute()逻辑
    saveFile();
});

4. 分布式系统中的远程命令

  • 场景:将命令序列化为 JSON/Protobuf,通过 RPC 发送到远程节点执行

  • 关键实现:

    java 复制代码
    // 命令需实现Serializable
    public class RemoteCommand implements Serializable {
        private String methodName;
        private Object[] params;
        // 省略getter/setter
    }
    
    // 远程调用者
    public Object executeRemoteCommand(RemoteCommand cmd) throws Exception {
        // 通过Socket/RestTemplate发送命令到服务端
        return remoteService.invoke(cmd);
    }

五、避坑指南:正确使用命令模式的 3 个要点

1. 避免过度封装简单操作

  • ❌ 反模式:对单一方法调用(如delete())强行封装命令
  • ✅ 最佳实践:仅在需要撤销、日志、批处理等高级功能时使用

2. 处理撤销的幂等性

  • 撤销操作需保证幂等(多次撤销不影响结果),建议:
    • 记录命令执行前的完整状态(如计算器的previousResult
    • 使用版本号或时间戳确保撤销的正确性

3. 控制命令类的数量

  • 大量具体命令可能导致类爆炸,建议:
    • 使用参数化命令(如ParametricCommand支持不同操作数)
    • 结合工厂模式统一管理命令创建

4. 反模式:混淆命令与业务逻辑

  • 命令对象应聚焦 "如何执行操作",而非 "操作本身的业务规则"
  • 复杂业务逻辑应封装在接收者(Receiver)或独立服务中

六、总结:何时该用命令模式?

适用场景 核心特征 典型案例
需要支持撤销 / 重做功能 操作具有可逆性,需记录历史状态 文本编辑器(Ctrl+Z)、版本控制系统
分布式远程调用 请求需序列化传输,支持异步执行 RPC 框架、消息队列(MQ)
批处理与事务性操作 需要批量执行并保证原子性 银行转账(批量操作回滚)
解耦请求发送与具体执行 发送者与执行者需要松耦合 事件驱动架构、任务调度系统

命令模式通过 "请求封装 + 责任分离" 的设计,将操作的 "触发" 与 "执行" 解耦,为系统添加了强大的扩展性(如支持撤销、日志、分布式调度)。下一篇我们将深入探讨迭代器模式,解析如何统一不同数据结构的遍历方式,敬请期待!

扩展思考:命令模式 vs 策略模式

两者都通过封装实现解耦,但核心目标不同:

模式 封装对象 核心功能 状态管理
命令模式 具体请求(如 "保存""撤销") 支持请求的记录与回退 需保存执行前 / 后状态
策略模式 算法或策略(如 "排序算法") 动态切换算法实现 无状态或仅共享上下文

理解这种差异,能帮助我们在设计时选择更合适的模式来解决实际问题。

相关推荐
520拼好饭被践踏几秒前
JAVA+Agent学习day26
java·开发语言·数据结构·学习·agent
郝学胜-神的一滴12 分钟前
力扣 692:巧用小顶堆高效求解前K个高频单词
java·数据结构·python·程序人生·算法·leetcode·职场和发展
zzzll111122 分钟前
SpringBoot 入门与实践指南
java·spring boot·后端
誰能久伴不乏27 分钟前
深入解析 C++ STL 底层原理:从设计哲学到内存收割机
c++·架构·stl
newerp1 小时前
Go :结构体嵌入、sort.Interface 与 io.Reader/Writer
后端
SamDeepThinking1 小时前
如何维持缓存的一致性?
后端·面试·程序员
泡海椒1 小时前
还在手动把 Postman 和浏览器的 curl 转成 Java 代码?这个框架让你一键粘贴直接用
后端
用户8181870627461 小时前
第13章 死锁诊断实战
后端
晨曦中的暮雨1 小时前
Redis 有哪些常见数据结构?它们的底层实现和使用场景是什么?
java·后端·八股
枫叶V1 小时前
Prompt Injection 防不住怎么办?从 Source-Sink 模型设计 Agent 安全边界
后端·agent