文章目录
-
- 前言
- [1. 命令模式是什么?](#1. 命令模式是什么?)
- [2. 命令模式解决什么问题?](#2. 命令模式解决什么问题?)
- [3. 核心结构](#3. 核心结构)
-
- [3.1 Command(命令接口)](#3.1 Command(命令接口))
- [3.2 ConcreteCommand(具体命令)](#3.2 ConcreteCommand(具体命令))
- [3.3 Receiver(接收者)](#3.3 Receiver(接收者))
- [3.4 Invoker(调用者)](#3.4 Invoker(调用者))
- [3.5 Client(客户端)](#3.5 Client(客户端))
- [4. 实现思路](#4. 实现思路)
- [5. 示例](#5. 示例)
-
- [5.1 Command:命令接口](#5.1 Command:命令接口)
- [5.2 Receiver:接收者(灯)](#5.2 Receiver:接收者(灯))
- [5.3 ConcreteCommand:具体命令](#5.3 ConcreteCommand:具体命令)
-
- [5.3.1 开灯命令](#5.3.1 开灯命令)
- [5.3.2 关灯命令](#5.3.2 关灯命令)
- [5.4 Invoker:调用者(遥控器)](#5.4 Invoker:调用者(遥控器))
- [5.5 Client:组装并使用](#5.5 Client:组装并使用)
- [6. 宏命令(批量执行)](#6. 宏命令(批量执行))
- [7. 优缺点](#7. 优缺点)
-
- [7.1 优点](#7.1 优点)
- [7.2 缺点](#7.2 缺点)
- [8. 和其他模式怎么区分?](#8. 和其他模式怎么区分?)
-
- [8.1 命令模式 vs 策略模式](#8.1 命令模式 vs 策略模式)
- [8.2 命令模式 vs 责任链模式](#8.2 命令模式 vs 责任链模式)
- [8.3 命令模式 vs 备忘录模式](#8.3 命令模式 vs 备忘录模式)
- [9. 适用场景](#9. 适用场景)
- [10. Java 中的实际应用](#10. Java 中的实际应用)
- [11. 总结](#11. 总结)
前言
在日常开发中,我们经常遇到"调用方想执行一个操作,但不想(也不该)直接依赖执行方"的场景,比如:
- 遥控器控制家电:按下按钮 → 开灯 / 关灯 / 调温度,遥控器不需要知道灯和空调的内部实现
- 文本编辑器:用户点击"撤销" → 回退上一步操作,编辑器需要记住每一步做了什么
- 任务队列:把多个操作排成队列,按顺序执行,甚至可以延迟执行
- 事务系统:一组操作要么全部成功,要么全部回滚
这些场景有一个共同点:操作本身被当作"东西"来传递、存储、排队、撤销,而不是简单地直接调用一个方法。
命令模式(Command Pattern)要解决的核心就是:
将"请求"封装成一个对象,使得调用方和执行方彻底解耦;同时让请求可以被存储、排队、记录日志,以及支持撤销/重做。
1. 命令模式是什么?
命令模式是一种行为型设计模式,它把"做什么"这件事抽象成一个对象(命令对象),从而做到:
- 调用方(Invoker)只管"发出命令",不关心谁来执行、怎么执行
- 执行方(Receiver)只管"干活",不关心谁下达的命令
- 命令对象是两者之间的桥梁,封装了"对谁做什么"的全部信息
- 命令对象可以被存起来、排队、撤销、重做、组合
2. 命令模式解决什么问题?
- 调用方和执行方之间存在强耦合,调用方直接调用执行方的方法,改一个就得改另一个
- 需要支持**撤销(undo)/ 重做(redo)**操作
- 需要将操作排队执行 或延迟执行
- 需要记录操作日志,用于审计或系统恢复
- 需要将多个操作组合成一个宏命令,批量执行
如果发现代码里"发起操作的人"和"执行操作的人"绑得太死,或者需要对操作做撤销、排队、日志等额外处理,就很适合命令模式。
3. 核心结构
3.1 Command(命令接口)
声明执行操作的接口,通常包含 execute() 方法,需要撤销时还会有 undo() 方法。
3.2 ConcreteCommand(具体命令)
实现命令接口,内部持有一个 Receiver 的引用,execute() 里调用 Receiver 的具体方法。它是"请求"的载体,封装了"对谁做什么"。
3.3 Receiver(接收者)
真正干活的对象,知道如何执行具体的业务操作。命令对象只是"转发"请求给它。
3.4 Invoker(调用者)
持有命令对象,在合适的时机调用命令的 execute() 方法。它不知道命令具体做了什么,只管"触发"。
3.5 Client(客户端)
负责创建具体命令对象、设置接收者、把命令交给调用者。
4. 实现思路
- 定义
Command接口(execute()+ 可选的undo()) - 写多个
ConcreteCommand(每个封装一个具体操作,内部持有 Receiver) - 写
Receiver(真正执行业务逻辑的类) - 写
Invoker(持有 Command 引用,负责触发执行) - 客户端组装:创建 Receiver → 创建 Command 并绑定 Receiver → 把 Command 交给 Invoker
5. 示例
智能家居遥控器:通过遥控器控制灯光,支持开灯、关灯和撤销操作
5.1 Command:命令接口
java
public interface Command {
void execute();
void undo();
}
5.2 Receiver:接收者(灯)
java
public class Light {
private String location;
public Light(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " 的灯已打开");
}
public void off() {
System.out.println(location + " 的灯已关闭");
}
}
5.3 ConcreteCommand:具体命令
5.3.1 开灯命令
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(); // 开灯的撤销就是关灯
}
}
5.3.2 关灯命令
java
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(); // 关灯的撤销就是开灯
}
}
5.4 Invoker:调用者(遥控器)
java
public class RemoteControl {
private Command command;
private Command lastCommand; // 记录上一次执行的命令,用于撤销
public RemoteControl() {
this.lastCommand = new NoCommand(); // 空命令,避免 null 判断
}
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
lastCommand = command;
}
public void pressUndo() {
System.out.println("--- 执行撤销 ---");
lastCommand.undo();
}
// 空命令对象,什么都不做
private static class NoCommand implements Command {
@Override
public void execute() { }
@Override
public void undo() { }
}
}
5.5 Client:组装并使用
java
public class Client {
public static void main(String[] args) {
// 创建接收者
Light livingRoomLight = new Light("客厅");
Light bedroomLight = new Light("卧室");
// 创建命令
Command livingRoomOn = new LightOnCommand(livingRoomLight);
Command livingRoomOff = new LightOffCommand(livingRoomLight);
Command bedroomOn = new LightOnCommand(bedroomLight);
// 创建调用者(遥控器)
RemoteControl remote = new RemoteControl();
// 开客厅灯
System.out.println("=== 开客厅灯 ===");
remote.setCommand(livingRoomOn);
remote.pressButton();
// 关客厅灯
System.out.println("=== 关客厅灯 ===");
remote.setCommand(livingRoomOff);
remote.pressButton();
// 撤销上一步(关灯的撤销 = 开灯)
remote.pressUndo();
// 开卧室灯
System.out.println("=== 开卧室灯 ===");
remote.setCommand(bedroomOn);
remote.pressButton();
}
}
输出:
=== 开客厅灯 ===
客厅 的灯已打开
=== 关客厅灯 ===
客厅 的灯已关闭
--- 执行撤销 ---
客厅 的灯已打开
=== 开卧室灯 ===
卧室 的灯已打开
会发现:
- 遥控器(Invoker)完全不知道灯是怎么开关的,它只管调用
command.execute() - 灯(Receiver)也不知道是谁让它开关的,它只管执行自己的
on()/off() - 命令对象是两者之间的"中间人",封装了"对哪个灯做什么操作"
- 撤销功能自然而然就有了------每个命令知道自己的反操作
- 如果将来要新增"调节亮度"命令,只需新增一个
DimCommand类,遥控器和灯的代码都不用改
6. 宏命令(批量执行)
命令模式还有一个很实用的变体------宏命令,可以把多个命令组合成一个,一键执行:
java
public class MacroCommand implements Command {
private Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
@Override
public void undo() {
// 逆序撤销
for (int i = commands.length - 1; i >= 0; i--) {
commands[i].undo();
}
}
}
使用:
java
// "回家模式":一键开客厅灯 + 开卧室灯
Command[] homeCommands = { livingRoomOn, bedroomOn };
Command homeMode = new MacroCommand(homeCommands);
remote.setCommand(homeMode);
remote.pressButton(); // 两个灯同时打开
remote.pressUndo(); // 两个灯同时关闭(逆序撤销)
7. 优缺点
7.1 优点
- 解耦调用方和执行方:Invoker 不依赖 Receiver,两者通过 Command 接口交互
- 支持撤销/重做:每个命令对象知道如何撤销自己,配合栈结构可以实现多级撤销
- 支持排队和延迟执行:命令是对象,可以放进队列、线程池、定时器
- 支持宏命令:多个命令可以组合成一个复合命令,批量执行
- 符合开闭原则:新增命令只需新增类,不改已有代码
- 支持日志和恢复:命令可以被序列化存储,用于审计或系统崩溃后恢复
7.2 缺点
- 类数量膨胀:每个操作都要写一个具体命令类,操作多了类会很多
- 简单场景过度设计:如果只是简单调用一个方法,用命令模式反而增加复杂度
- 性能开销:频繁创建命令对象可能带来额外的内存和 GC 压力
8. 和其他模式怎么区分?
8.1 命令模式 vs 策略模式
- 策略模式:封装的是"算法",关注的是"用哪种方式做",客户端主动选择策略
- 命令模式:封装的是"请求",关注的是"做什么",还支持撤销、排队、日志等额外能力
8.2 命令模式 vs 责任链模式
- 责任链:请求沿链传递,由某个节点处理,强调"谁来处理"
- 命令模式:请求被封装成对象,由 Invoker 触发执行,强调"请求本身的对象化"
8.3 命令模式 vs 备忘录模式
- 备忘录模式:保存对象的状态快照,用于恢复状态
- 命令模式:保存的是"操作"本身,通过 undo 反向执行来恢复。两者经常配合使用------备忘录保存命令执行前的状态,命令的 undo 利用备忘录来恢复
9. 适用场景
满足以下情况时,命令模式很合适:
- 需要将请求的发起方和执行方解耦
- 需要支持撤销(undo)和重做(redo)
- 需要将操作排队执行 或延迟执行
- 需要记录操作日志,用于审计或系统恢复
- 需要将多个操作组合成宏命令批量执行
- 需要支持事务------一组操作要么全部成功,要么全部回滚
10. Java 中的实际应用
命令模式在 Java 生态中非常常见:
java
// Java 中最经典的命令模式:Runnable 接口
// Runnable 就是一个命令对象,Thread 就是 Invoker
Runnable command = () -> System.out.println("执行任务");
Thread invoker = new Thread(command);
invoker.start();
常见的命令模式应用:
java.lang.Runnable:最简单的命令接口,Thread是 Invokerjavax.swing.Action:Swing 中按钮点击事件的命令封装java.util.concurrent.Callable:带返回值的命令接口- Spring 的
TransactionTemplate:将事务操作封装为命令 - Netflix Hystrix 的
HystrixCommand:将远程调用封装为命令,支持熔断和降级 - Android 的
Handler+Runnable:将 UI 操作封装为命令投递到主线程执行
11. 总结
命令模式通过"命令接口 + 具体命令 + 接收者 + 调用者"的结构,将请求封装成独立的对象,彻底解耦了请求的发起方和执行方。命令对象不仅可以被执行,还可以被存储、排队、撤销、组合,让系统在操作管理上获得极大的灵活性。当你需要对"操作"本身做文章(撤销、排队、日志、事务)时,命令模式就是你的首选。
