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

发布于:2025-04-20 ⋅ 阅读:(57) ⋅ 点赞:(0)

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

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

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

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

核心思想与 UML 类图

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

PlantUML Diagram

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

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

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

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

加法命令
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); // 恢复执行前的状态
    }
}
减法命令
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. 接收者(实际执行操作的实体)

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. 调用者(触发命令执行)

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. 客户端调用示例(支持撤销的计算)

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. 命令队列(支持批量执行与撤销)

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. 持久化命令日志(支持故障恢复)

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. 可视化命令流程

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

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

1. Spring MVC 的 Command 对象

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

2. Struts 2 的 Action 机制

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

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

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

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

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

  • 关键实现:

    // 命令需实现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 策略模式

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

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

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


网站公告

今日签到

点亮在社区的每一天
去签到