Java 23 种设计模式:从踩坑到精通 | 命令模式 —— 把操作封装成对象,实现撤销与排队

Java 23 种设计模式:从踩坑到精通 | 命令模式 ------ 把操作封装成对象,实现撤销与排队

摘要 :当需要将请求的发起者与执行者解耦,或需要支持命令的撤销、排队、日志记录时,直接在调用处写业务逻辑会导致代码高度耦合且难以扩展。命令模式通过将请求封装为独立的对象,让请求的发送者与接收者完全解耦,从而支持参数化命令、命令队列、撤销/重做等高级功能。本文从智能家居遥控器的场景出发,完整讲解命令模式的原理、UML、代码实现、与策略模式的区别,并结合 JDK Runnable、Spring JDBC、消息队列等框架应用,帮你掌握"将行为对象化"的设计精髓。
🗺️ 本文阅读地图(3 分钟速览)

  • 为什么遥控器按钮不能写死?
  • 命令模式四大角色拆解
  • 手写万能遥控器(支持多步撤销 + Lambda 简化)
  • 宏命令:一键执行多个操作(含异常处理)
  • JDK Runnable / 线程池如何体现命令模式
  • 面试必问:命令 vs 策略,到底怎么区分?
    📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:责任链模式 | 当前:命令模式 | 下一篇:解释器模式

🔗 返回系列总目录


文章目录

  • [Java 23 种设计模式:从踩坑到精通 | 命令模式 ------ 把操作封装成对象,实现撤销与排队](#Java 23 种设计模式:从踩坑到精通 | 命令模式 —— 把操作封装成对象,实现撤销与排队)
    • [1. 从一个"万能遥控器"的需求说起](#1. 从一个“万能遥控器”的需求说起)
      • [1.1 你的场景该不该用命令模式?](#1.1 你的场景该不该用命令模式?)
    • [2. 模式定义与 UML 结构](#2. 模式定义与 UML 结构)
      • [图文解析(配合上述 UML 图)](#图文解析(配合上述 UML 图))
    • [3. 代码实现:智能家居遥控器](#3. 代码实现:智能家居遥控器)
      • [3.1 接收者:各种家电](#3.1 接收者:各种家电)
      • [3.2 抽象命令](#3.2 抽象命令)
      • [3.3 具体命令(传统实现)](#3.3 具体命令(传统实现))
      • [3.4 调用者:遥控器(支持多步撤销)](#3.4 调用者:遥控器(支持多步撤销))
      • [3.5 客户端](#3.5 客户端)
      • [3.6 Lambda 简化版(Java 8+)](#3.6 Lambda 简化版(Java 8+))
    • [4. 进阶:宏命令(一键执行多个命令,含异常处理)](#4. 进阶:宏命令(一键执行多个命令,含异常处理))
    • [5. 命令模式 vs 策略模式](#5. 命令模式 vs 策略模式)
    • [6. 优缺点一览](#6. 优缺点一览)
    • [7. 框架与实践中的应用](#7. 框架与实践中的应用)
      • [7.1 JDK:Runnable / Callable](#7.1 JDK:Runnable / Callable)
      • [7.2 Spring JDBC:JdbcTemplate](#7.2 Spring JDBC:JdbcTemplate)
      • [7.3 消息队列 / 作业调度](#7.3 消息队列 / 作业调度)
    • [8. 面试必问 + 面试官追问连环炮](#8. 面试必问 + 面试官追问连环炮)
    • [9. 六大设计原则在命令模式中的体现](#9. 六大设计原则在命令模式中的体现)
    • [10 实战案例:电子面单多平台对接](#10 实战案例:电子面单多平台对接)
    • [《Java 23 种设计模式:从踩坑到精通》快速导航](#《Java 23 种设计模式:从踩坑到精通》快速导航)

1. 从一个"万能遥控器"的需求说起

假设你在开发一款智能家居遥控器,它需要控制电灯、空调、电视等多种设备,每个设备有开和关两个操作。如果直接在遥控器类中写死:

java 复制代码
if (button.equals("light_on")) {
    light.on();
} else if (button.equals("ac_on")) {
    ac.on();
}
// ...

遥控器与具体设备高度耦合,新增设备或操作都需要修改遥控器代码。更复杂的是,用户希望遥控器支持撤销上一次操作一键执行多个命令 ,这用 if-else 几乎无法优雅实现。

命令模式(Command Pattern)正是为解决这类问题而生的:它将"请求"封装成对象,从而让你可以用不同的请求对客户端参数化、对请求排队或记录请求日志,以及支持可撤销的操作。

1.1 你的场景该不该用命令模式?

判断标准 是 → 用命令模式 否 → 用其他方式
需要将请求的发起者与执行者解耦
需要支持撤销/重做、命令队列、宏命令
需要在运行时动态指定、排列或执行请求
只有简单的调用,无需撤销或排队 直接调用即可

2. 模式定义与 UML 结构

命令模式 将一个请求封装为一个对象,从而使你可以用不同的请求对客户端进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。它属于 行为型设计模式

图文解析(配合上述 UML 图)

命令模式的核心角色如下:

  • 抽象命令(Command :定义统一的 execute()undo() 接口,让所有命令都可被调用和撤销。
  • 具体命令(LightOnCommand / LightOffCommand :将接收者 与一个动作绑定,实现 execute() 去调用接收者的方法,undo() 执行反向操作。
  • 接收者(Light:知道如何实施与执行一个请求相关的操作,是真正干活的对象。
  • 调用者(Invoker :持有命令对象,在某个时间点调用命令的 execute()。它不需要知道命令是如何实现的,只需按下按钮。

核心机制 :命令模式把"调用"这个行为对象化,让 InvokerReceiver 之间没有直接依赖,只有命令对象作为中间人。


3. 代码实现:智能家居遥控器

3.1 接收者:各种家电

java 复制代码
// 电灯
public class Light {
    public void on() { System.out.println("电灯打开"); }
    public void off() { System.out.println("电灯关闭"); }
}

// 空调
public class AirConditioner {
    public void on() { System.out.println("空调打开,制冷模式 26°C"); }
    public void off() { System.out.println("空调关闭"); }
}

// 电视
public class TV {
    public void on() { System.out.println("电视打开"); }
    public void off() { System.out.println("电视关闭"); }
}

💬 白话:这些就是真正被控制的设备,每个设备都有自己的开关逻辑。

3.2 抽象命令

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

💬 白话:所有命令都必须有"执行"和"撤销"两种能力。

3.3 具体命令(传统实现)

java 复制代码
// 开灯命令
public class LightOnCommand implements Command {
    private Light light;

    public LightOnCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.on(); }

    @Override
    public void undo() { light.off(); }  // 反向操作
}

// 关灯命令
public class LightOffCommand implements Command {
    private Light light;

    public LightOffCommand(Light light) { this.light = light; }

    @Override
    public void execute() { light.off(); }

    @Override
    public void undo() { light.on(); }
}

// 开空调命令
public class ACOnCommand implements Command {
    private AirConditioner ac;

    public ACOnCommand(AirConditioner ac) { this.ac = ac; }

    @Override
    public void execute() { ac.on(); }

    @Override
    public void undo() { ac.off(); }
}

// 关空调命令
public class ACOffCommand implements Command {
    private AirConditioner ac;

    public ACOffCommand(AirConditioner ac) { this.ac = ac; }

    @Override
    public void execute() { ac.off(); }

    @Override
    public void undo() { ac.on(); }
}

💬 白话 :每个命令都知道自己操作哪个设备,并定义好反向操作,这样撤销时直接调用 undo() 即可。

3.4 调用者:遥控器(支持多步撤销)

使用 Stack<Command> 替代单个变量,实现连续撤销。

java 复制代码
public class RemoteControl {
    private Command[] onCommands;
    private Command[] offCommands;
    private Stack<Command> undoStack = new Stack<>();   // 支持多步撤销
    private static final int MAX_UNDO = 20;              // 限制最大撤销步数

    public RemoteControl(int slotCount) {
        onCommands = new Command[slotCount];
        offCommands = new Command[slotCount];
        Command noCommand = new NoCommand();
        for (int i = 0; i < slotCount; i++) {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }

    public void setCommand(int slot, Command onCommand, Command offCommand) {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    public void pressOnButton(int slot) {
        onCommands[slot].execute();
        pushUndo(onCommands[slot]);
    }

    public void pressOffButton(int slot) {
        offCommands[slot].execute();
        pushUndo(offCommands[slot]);
    }

    public void pressUndoButton() {
        if (!undoStack.isEmpty()) {
            Command lastCommand = undoStack.pop();
            lastCommand.undo();
        } else {
            System.out.println("没有可撤销的操作");
        }
    }

    private void pushUndo(Command command) {
        undoStack.push(command);
        // 限制栈大小,避免内存溢出
        if (undoStack.size() > MAX_UNDO) {
            undoStack.remove(0);
        }
    }
}

// 空命令(避免 null 判断)
class NoCommand implements Command {
    @Override public void execute() {}
    @Override public void undo() {}
}

💬 白话 :遥控器把执行过的命令存入栈中,撤销时弹出最近一条命令并调用其 undo(),实现多步撤销。

3.5 客户端

java 复制代码
Light livingRoomLight = new Light();
AirConditioner ac = new AirConditioner();

LightOnCommand lightOn = new LightOnCommand(livingRoomLight);
LightOffCommand lightOff = new LightOffCommand(livingRoomLight);
ACOnCommand acOn = new ACOnCommand(ac);
ACOffCommand acOff = new ACOffCommand(ac);

RemoteControl remote = new RemoteControl(2);
remote.setCommand(0, lightOn, lightOff);
remote.setCommand(1, acOn, acOff);

remote.pressOnButton(0);   // 电灯打开
remote.pressOnButton(1);   // 空调打开
remote.pressUndoButton();  // 空调关闭
remote.pressUndoButton();  // 电灯关闭

3.6 Lambda 简化版(Java 8+)

对于逻辑简单的命令,不必为每一个操作新建类,可以用函数式接口直接创建命令。

java 复制代码
public class FunctionalRemoteControl {
    private Command[] onCommands;
    private Command[] offCommands;
    private Stack<Command> undoStack = new Stack<>();

    public FunctionalRemoteControl(int slotCount) {
        onCommands = new Command[slotCount];
        offCommands = new Command[slotCount];
        Command noCommand = () -> {};
        Arrays.fill(onCommands, noCommand);
        Arrays.fill(offCommands, noCommand);
    }

    // 直接传入 Runnable 作为执行逻辑
    public void setOnCommand(int slot, Runnable action, Runnable undoAction) {
        onCommands[slot] = new Command() {
            @Override
            public void execute() { action.run(); }
            @Override
            public void undo() { undoAction.run(); }
        };
    }
    // ... 其余方法类似
}

💬 白话:简单操作不再需要单独的类文件,直接用 Lambda 表达式描述"做什么"和"如何撤销"。


4. 进阶:宏命令(一键执行多个命令,含异常处理)

java 复制代码
public class MacroCommand implements Command {
    private List<Command> commands;

    public MacroCommand(List<Command> commands) { this.commands = commands; }

    @Override
    public void execute() {
        for (Command cmd : commands) {
            try {
                cmd.execute();
            } catch (Exception e) {
                // 简单处理:记录失败并继续执行后续命令
                System.err.println("命令执行失败: " + e.getMessage());
                // 若需原子性,可在此实现预检查或补偿逻辑
            }
        }
    }

    @Override
    public void undo() {
        // 逆序撤销,遇到失败同样记录并继续
        for (int i = commands.size() - 1; i >= 0; i--) {
            try {
                commands.get(i).undo();
            } catch (Exception e) {
                System.err.println("撤销失败: " + e.getMessage());
            }
        }
    }
}

💬 白话:宏命令打包多个命令,一键执行,撤销时倒序撤销。增加了异常处理,避免中间失败导致流程中断。

客户端使用:

java 复制代码
List<Command> partyCommands = Arrays.asList(lightOn, acOn);
MacroCommand partyMode = new MacroCommand(partyCommands);

remote.setCommand(2, partyMode, new NoCommand());
remote.pressOnButton(2);   // 电灯打开 → 空调打开
remote.pressUndoButton();  // 空调关闭 → 电灯关闭

5. 命令模式 vs 策略模式

对比维度 命令模式 策略模式
目的 将请求封装为对象,解耦调用者与执行者 封装一系列算法,使其可相互替换
侧重点 请求的封装与传递,支持撤销/队列 算法的封装与替换
典型应用 遥控器、作业队列、撤销操作 支付方式、排序算法、折扣策略
是否关注撤销

💡 简单记忆:命令模式是"行为对象化",策略模式是"算法对象化"。策略模式通常需要一个上下文(Context)持有当前策略并调用算法,而命令模式则由调用者发起动作,更强调"动作"本身。


6. 优缺点一览

优点 缺点
解耦:调用者与接收者完全解耦 类膨胀:每个命令都是一个类,命令多时类数量大增
可扩展:新增命令无需修改现有代码,符合开闭原则 理解成本:角色较多,初学时不易理解
支持撤销/重做:通过状态栈实现多步撤销 命令对象若持有可变状态,在多线程下需要额外同步
支持组合:宏命令可将多个命令组合执行

⚠️ 线程安全提醒 :如果命令对象本身持有可变状态(如记录操作前数据),在多线程环境下需保证其不可变或使用同步机制。推荐将命令设计为无状态,状态由外部传入或由接收者管理,这样可以安全地在线程池中执行。


7. 框架与实践中的应用

7.1 JDK:Runnable / Callable

Runnable 接口就是典型的命令模式。线程池中的线程是 InvokerRunnableCommand,具体业务逻辑在 run() 中实现。

java 复制代码
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(() -> System.out.println("执行任务")); // 命令被线程池调度执行

7.2 Spring JDBC:JdbcTemplate

JdbcTemplate 使用命令模式将 SQL 操作封装为 PreparedStatementCallbackResultSetExtractor 等回调对象,由 JdbcTemplate 统一调用。

7.3 消息队列 / 作业调度

消息队列中的消息本质上就是序列化的命令对象。消费者接收到消息后执行 execute(),支持异步处理、重试、顺序执行。


8. 面试必问 + 面试官追问连环炮

基础必问

  • 命令模式的四个角色? → Command、ConcreteCommand、Invoker、Receiver。
  • Runnable 是什么模式? → 命令模式。
  • 如何实现撤销? → 在命令对象中存储操作前的状态,或执行反向操作,配合栈实现多步撤销。
  • 命令模式和策略模式的区别? → 命令关注请求的封装与调用者解耦,策略关注算法的封装与替换。

面试官追问

  • "宏命令的撤销为什么是逆序?"
    👉 因为后执行的操作依赖先执行的结果,逆序撤销才能恢复正确状态。
  • "命令模式怎么实现重试?"
    👉 把命令对象放入队列,执行失败时重新入队,或通过 execute() 内部循环重试。
  • "命令模式和观察者模式能配合吗?"
    👉 可以。触发命令执行后,通过观察者通知 UI 更新状态(如按钮颜色变化)。

🎉 恭喜 :如果你能立刻说出 Runnable 是命令模式,并理解宏命令的逆序撤销机制,你已经掌握了行为型模式中最灵活的"请求封装"设计。


9. 六大设计原则在命令模式中的体现

设计原则 在命令模式中的体现
单一职责原则(SRP) 命令类只负责封装请求,接收者只负责执行
开闭原则(OCP) 新增命令无需修改 Invoker 或 Receiver
里氏替换原则(LSP) 所有命令都遵循 Command 接口,可相互替换
依赖倒置原则(DIP) Invoker 依赖抽象 Command,不依赖具体命令
接口隔离原则(ISP) Command 接口只定义 execute()undo(),精简
迪米特法则(LoD) 客户端只需配置命令,无需知道 Receiver 的细节

💡 进阶思考:命令模式是事件溯源(Event Sourcing)和 AI Agent 工具调用的基础。将命令对象持久化,你就能重现系统的任何历史状态;在 AI 应用中,模型输出的"动作指令"可被转换为命令对象执行,实现可插拔的工具集成。


10 实战案例:电子面单多平台对接

本文讲解的设计模式,已在真实电商项目中落地。如果你想看这些模式在十余个平台、日均10w+订单场景下的完整应用,欢迎阅读我的电子面单实战系列:

📖 电商多平台电子面单对接实战


《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇:解释器模式:自己动手写一个小语言解释器!🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。