文章目录
- 一、概述
-
- [1.1 结构与角色](#1.1 结构与角色)
- [1.2 适用场景](#1.2 适用场景)
- 二、实现方式
-
- [2.1 基本命令](#2.1 基本命令)
- [2.2 宏命令](#2.2 宏命令)
- [2.3 撤销与恢复](#2.3 撤销与恢复)
- 三、总结
一、概述
在软件开发中,经常会遇到这样的场景:需要对某个操作进行"请求化"管理------将请求封装为对象,从而支持参数化、队列化、日志化 ,以及撤销/恢复 等功能。例如,遥控器上每个按键对应一个操作,但遥控器本身不需要知道操作的具体实现;文本编辑器的撤销功能需要记录每一步操作;任务调度系统需要将任务放入队列异步执行。如果让调用者直接调用接收者的方法,就会产生紧耦合------调用者必须知道接收者的所有细节,且无法对操作进行统一管理:
直接调用 open()
直接调用 turnOn()
直接调用 start()
直接调用 play()
调用者
电视
电灯
空调
音响
调用者与每个设备强耦合,新增设备需修改调用者
命令模式(Command Pattern)正是为了解决这个问题而诞生的------它将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。命令模式将"发出请求的对象"和"执行请求的对象"完全解耦。
生活中的命令模式例子:
- 餐厅点餐:顾客(Client)把点餐单(Command)交给服务员(Invoker),服务员把点餐单递给厨师(Receiver),厨师根据点餐单做菜。顾客不需要直接和厨师沟通,服务员也只负责传递点餐单
- 遥控器:遥控器(Invoker)上的每个按键对应一个命令对象(Command),按下按键时执行对应的命令,遥控器本身不关心命令具体做什么
- 文本编辑器撤销:每次编辑操作被封装为一个命令对象,编辑器的撤销栈保存所有命令,撤销时按相反顺序执行命令的逆向操作
- 任务队列:线程池中的任务(Runnable / Callable)就是命令对象,线程池只管调度执行,不关心任务的具体逻辑
核心:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作
1.1 结构与角色
命令模式包含以下角色:
持有并调用
实现
调用
设置命令
Client 客户端
Receiver 接收者
ConcreteCommand 具体命令
Invoker 调用者
Command 命令接口
- Command(命令接口) :声明执行操作的接口,通常包含
execute()方法,有时也包含undo()方法 - ConcreteCommand(具体命令):实现命令接口,将一个接收者对象绑定于一个动作,调用接收者相应的操作
- Invoker(调用者) :持有命令对象,在某个时间点调用命令对象的
execute()方法执行请求 - Receiver(接收者):知道如何实施与执行一个请求相关的具体操作,任何类都可能作为一个接收者
- Client(客户端):创建一个具体命令对象并设定它的接收者,同时将命令对象设置给调用者
1.2 适用场景
- 需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
- 需要在不同的时间指定请求、将请求排队和执行请求
- 需要支持撤销操作和恢复操作
- 需要支持宏命令------将一组操作组合为一个复合命令
- 需要将系统的操作记录到日志中,以便在系统崩溃时重新执行
二、实现方式
命令模式的核心实现思路是:将"动作"封装为独立的命令对象,命令对象持有接收者的引用,调用者通过命令接口与具体命令交互,从而将"请求的发起"与"请求的执行"彻底解耦。
2.1 基本命令
以"智能家居遥控器"为例,遥控器上有多个按钮,每个按钮控制一种家电(电视、电灯、音响),遥控器只需知道"执行"操作,无需了解设备的具体控制细节:
持有
实现
实现
实现
实现
调用
调用
客户端
TV 电视
Light 电灯
RemoteControl 遥控器
Command 命令接口
TVOnCommand 开电视
TVOffCommand 关电视
LightOnCommand 开灯
LightOffCommand 关灯
(1)命令接口
java
/**
* 命令接口
* 所有具体命令必须实现此接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
}
(2)接收者------电视
java
/**
* 接收者:电视
* 提供电视的具体操作
*/
public class TV {
private final String location;
public TV(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " 电视:已打开");
}
public void off() {
System.out.println(location + " 电视:已关闭");
}
public void setChannel(int channel) {
System.out.println(location + " 电视:切换到 " + channel + " 频道");
}
}
(3)接收者------电灯
java
/**
* 接收者:电灯
* 提供电灯的具体操作
*/
public class Light {
private final String location;
public Light(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " 电灯:已打开");
}
public void off() {
System.out.println(location + " 电灯:已关闭");
}
}
(4)具体命令------开电视命令
java
/**
* 具体命令:打开电视
* 封装了打开电视的操作
*/
public class TVOnCommand implements Command {
private final TV tv;
public TVOnCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.on();
tv.setChannel(1);
}
}
(5)具体命令------关电视命令
java
/**
* 具体命令:关闭电视
* 封装了关闭电视的操作
*/
public class TVOffCommand implements Command {
private final TV tv;
public TVOffCommand(TV tv) {
this.tv = tv;
}
@Override
public void execute() {
tv.off();
}
}
(6)具体命令------开灯命令
java
/**
* 具体命令:打开电灯
* 封装了打开电灯的操作
*/
public class LightOnCommand implements Command {
private final Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on();
}
}
(7)具体命令------关灯命令
java
/**
* 具体命令:关闭电灯
* 封装了关闭电灯的操作
*/
public class LightOffCommand implements Command {
private final Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.off();
}
}
(8)调用者------遥控器
java
/**
* 调用者:遥控器
* 持有命令对象,负责在合适时机调用命令
*/
public class RemoteControl {
/** 最多 7 个按钮插槽 */
private static final int SLOT_COUNT = 7;
/** 每个插槽对应一个命令(开) */
private final Command[] onCommands = new Command[SLOT_COUNT];
/** 每个插槽对应一个命令(关) */
private final Command[] offCommands = new Command[SLOT_COUNT];
/**
* 为某个插槽设置开/关命令
*
* @param slot 插槽编号(0~6)
* @param onCommand 打开命令
* @param offCommand 关闭命令
*/
public void setCommand(int slot, Command onCommand, Command offCommand) {
if (slot < 0 || slot >= SLOT_COUNT) {
throw new IllegalArgumentException("插槽编号越界:" + slot);
}
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
/**
* 按下"开"按钮
*
* @param slot 插槽编号
*/
public void pressOnButton(int slot) {
if (onCommands[slot] != null) {
onCommands[slot].execute();
} else {
System.out.println("插槽 " + slot + " 未设置命令");
}
}
/**
* 按下"关"按钮
*
* @param slot 插槽编号
*/
public void pressOffButton(int slot) {
if (offCommands[slot] != null) {
offCommands[slot].execute();
} else {
System.out.println("插槽 " + slot + " 未设置命令");
}
}
}
(9)客户端调用
java
public class CommandDemo {
public static void main(String[] args) {
// 创建接收者(家电设备)
TV livingRoomTV = new TV("客厅");
Light kitchenLight = new Light("厨房");
// 创建命令对象
Command tvOn = new TVOnCommand(livingRoomTV);
Command tvOff = new TVOffCommand(livingRoomTV);
Command lightOn = new LightOnCommand(kitchenLight);
Command lightOff = new LightOffCommand(kitchenLight);
// 创建调用者(遥控器),绑定命令
RemoteControl remote = new RemoteControl();
remote.setCommand(0, tvOn, tvOff); // 插槽0:控制电视
remote.setCommand(1, lightOn, lightOff); // 插槽1:控制电灯
// 使用遥控器
remote.pressOnButton(0);
// 客厅 电视:已打开
// 客厅 电视:切换到 1 频道
remote.pressOnButton(1);
// 厨房 电灯:已打开
remote.pressOffButton(1);
// 厨房 电灯:已关闭
remote.pressOffButton(0);
// 客厅 电视:已关闭
}
}
关键点 :遥控器只依赖
Command接口,无需知道电视、电灯等具体设备的存在。新增一个家电(如空调),只需新增对应的命令类并设置到遥控器的空闲插槽即可,遥控器代码无需任何修改,符合开闭原则。
2.2 宏命令
宏命令(Macro Command)是一种特殊的命令,它内部持有一组命令的集合,执行宏命令时会依次执行其中的每个子命令。宏命令本质上也是实现了 Command 接口的类,因此可以像普通命令一样被调用者使用,也可以嵌套(宏命令中包含另一个宏命令)。
以"智能家居场景模式"为例,一键"回家模式"会自动打开客厅灯、打开电视、开启空调:
实现
持有
持有
持有
持有
execute
客户端
MacroCommand 宏命令
Command 接口
TVOnCommand 开电视
LightOnCommand 开灯
ACOnCommand 开空调
RemoteControl 遥控器
(1)宏命令
java
import java.util.ArrayList;
import java.util.List;
/**
* 宏命令
* 将多个命令组合为一个复合命令,按添加顺序依次执行
*/
public class MacroCommand implements Command {
private final List<Command> commands = new ArrayList<>();
/**
* 添加子命令
*
* @param command 子命令
* @return 当前宏命令(支持链式添加)
*/
public MacroCommand addCommand(Command command) {
commands.add(command);
return this;
}
/**
* 移除子命令
*
* @param command 子命令
*/
public void removeCommand(Command command) {
commands.remove(command);
}
@Override
public void execute() {
for (Command command : commands) {
command.execute();
}
}
}
(2)接收者------空调(新增设备)
java
/**
* 接收者:空调
*/
public class AC {
private final String location;
public AC(String location) {
this.location = location;
}
public void on() {
System.out.println(location + " 空调:已开启(制冷 26°C)");
}
public void off() {
System.out.println(location + " 空调:已关闭");
}
}
(3)具体命令------开空调命令
java
/**
* 具体命令:开启空调
*/
public class ACOnCommand implements Command {
private final AC ac;
public ACOnCommand(AC ac) {
this.ac = ac;
}
@Override
public void execute() {
ac.on();
}
}
(4)客户端调用------宏命令
java
public class MacroCommandDemo {
public static void main(String[] args) {
// 创建接收者
TV livingRoomTV = new TV("客厅");
Light livingRoomLight = new Light("客厅");
AC livingRoomAC = new AC("客厅");
// 创建宏命令:一键回家模式
MacroCommand homeMode = new MacroCommand();
homeMode.addCommand(new TVOnCommand(livingRoomTV))
.addCommand(new LightOnCommand(livingRoomLight))
.addCommand(new ACOnCommand(livingRoomAC));
// 创建遥控器,将宏命令绑定到插槽
RemoteControl remote = new RemoteControl();
remote.setCommand(0, homeMode, new NoCommand()); // 插槽0:回家模式
// 按一下,全部开启
remote.pressOnButton(0);
// 客厅 电视:已打开
// 客厅 电视:切换到 1 频道
// 客厅 电灯:已打开
// 客厅 空调:已开启(制冷 26°C)
}
}
(5)空命令------避免空指针检查
java
/**
* 空命令(Null Object 模式)
* 什么都不做,避免遥控器中判空,简化代码
*/
public class NoCommand implements Command {
@Override
public void execute() {
// 空实现
}
}
关键点 :宏命令实现了
Command接口,因此它可以像普通命令一样被遥控器持有和执行,这就是命令模式的强大之处------命令对象可以被任意组合、嵌套,形成树形结构。
2.3 撤销与恢复
命令模式最经典的应用之一就是撤销(Undo)和恢复(Redo) 。通过在命令接口中增加 undo() 方法,每个具体命令在 execute() 时保存必要的前置状态,undo() 时恢复状态。
以"文本编辑器"为例,支持输入、删除操作的撤销与恢复:
持有
维护栈
实现
实现
调用
调用
Editor 文本编辑器
CommandHistory 命令历史
undoStack / redoStack
WriteCommand 写入
Command 接口
DeleteCommand 删除
Document 文档
(1)带撤销的命令接口
java
/**
* 可撤销的命令接口
* 在基本 Command 的基础上增加 undo() 方法
*/
public interface UndoableCommand {
/**
* 执行命令
*/
void execute();
/**
* 撤销命令(逆向操作)
*/
void undo();
}
(2)接收者------文档
java
/**
* 接收者:文档
* 维护文本内容和光标位置
*/
public class Document {
private final StringBuilder content = new StringBuilder();
private int cursorPosition = 0;
/**
* 在光标位置插入文本
*
* @param text 要插入的文本
*/
public void insert(String text) {
if (text == null || text.isEmpty()) {
return;
}
content.insert(cursorPosition, text);
cursorPosition += text.length();
}
/**
* 删除光标位置前的指定长度文本
*
* @param length 删除长度
* @return 被删除的文本(用于撤销)
*/
public String delete(int length) {
if (length <= 0 || cursorPosition == 0) {
return "";
}
int start = Math.max(0, cursorPosition - length);
String deleted = content.substring(start, cursorPosition);
content.delete(start, cursorPosition);
cursorPosition = start;
return deleted;
}
public String getContent() {
return content.toString();
}
public int getCursorPosition() {
return cursorPosition;
}
public void setCursorPosition(int position) {
if (position < 0 || position > content.length()) {
throw new IllegalArgumentException("光标位置越界:" + position);
}
this.cursorPosition = position;
}
}
(3)具体命令------写入命令
java
/**
* 具体命令:写入文本
* 执行时插入文本并记录位置,撤销时删除刚插入的文本
*/
public class WriteCommand implements UndoableCommand {
private final Document document;
private final String text;
private int insertPosition;
public WriteCommand(Document document, String text) {
this.document = document;
this.text = text;
}
@Override
public void execute() {
// 保存插入前光标位置
insertPosition = document.getCursorPosition();
document.insert(text);
}
@Override
public void undo() {
// 撤销:定位到插入位置,删除刚插入的文本
document.setCursorPosition(insertPosition + text.length());
document.delete(text.length());
}
}
(4)具体命令------删除命令
java
/**
* 具体命令:删除文本
* 执行时删除指定长度文本并保存内容,撤销时重新插入
*/
public class DeleteCommand implements UndoableCommand {
private final Document document;
private final int length;
/** 被删除的文本(用于撤销时恢复) */
private String deletedText;
public DeleteCommand(Document document, int length) {
this.document = document;
this.length = length;
}
@Override
public void execute() {
deletedText = document.delete(length);
}
@Override
public void undo() {
if (deletedText != null && !deletedText.isEmpty()) {
document.insert(deletedText);
}
}
}
(5)命令历史------管理撤销/恢复栈
java
import java.util.Stack;
/**
* 命令历史管理器
* 维护撤销栈和恢复栈,支持多步撤销与恢复
*/
public class CommandHistory {
/** 撤销栈:记录已执行的命令 */
private final Stack<UndoableCommand> undoStack = new Stack<>();
/** 恢复栈:记录已撤销的命令 */
private final Stack<UndoableCommand> redoStack = new Stack<>();
/**
* 执行命令并将其记录到撤销栈
*
* @param command 要执行的命令
*/
public void executeCommand(UndoableCommand command) {
command.execute();
undoStack.push(command);
// 执行新命令后清空恢复栈(不能恢复已过期的操作)
redoStack.clear();
}
/**
* 撤销最近一次操作
*/
public void undo() {
if (!undoStack.isEmpty()) {
UndoableCommand command = undoStack.pop();
command.undo();
redoStack.push(command);
} else {
System.out.println("没有可撤销的操作");
}
}
/**
* 恢复最近一次撤销
*/
public void redo() {
if (!redoStack.isEmpty()) {
UndoableCommand command = redoStack.pop();
command.execute();
undoStack.push(command);
} else {
System.out.println("没有可恢复的操作");
}
}
}
(6)客户端调用------撤销与恢复
java
public class UndoDemo {
public static void main(String[] args) {
Document document = new Document();
CommandHistory history = new CommandHistory();
// 执行写入操作
history.executeCommand(new WriteCommand(document, "Hello"));
history.executeCommand(new WriteCommand(document, " World"));
System.out.println("当前内容:" + document.getContent());
// 当前内容:Hello World
// 撤销一步
history.undo();
System.out.println("撤销后:" + document.getContent());
// 撤销后:Hello
// 再撤销一步
history.undo();
System.out.println("再撤销:" + document.getContent());
// 再撤销:
// 恢复一步
history.redo();
System.out.println("恢复后:" + document.getContent());
// 恢复后:Hello
// 执行删除操作
history.executeCommand(new DeleteCommand(document, 5));
System.out.println("删除后:" + document.getContent());
// 删除后:
// 撤销删除
history.undo();
System.out.println("撤销删除:" + document.getContent());
// 撤销删除:Hello
}
}
关键点 :撤销机制的核心在于每个命令对象在执行时保存了足够的前置状态 ------
WriteCommand记录了插入位置,DeleteCommand保存了被删除的文本。撤销时只需调用undo()逆向操作即可。CommandHistory管理两个栈(undoStack 和 redoStack),实现多步撤销与恢复。
三、总结
命令模式是行为型设计模式中非常实用的一种,它将"请求"封装为对象,使请求的发送者与接收者完全解耦,并天然支持撤销/恢复、宏命令、任务队列等高级特性。本文围绕命令模式的核心思想,从基本命令到宏命令再到撤销恢复进行了系统讲解:
| 章节 | 核心内容 |
|---|---|
| 一、概述 | 介绍了命令模式的定义、结构和适用场景,通过餐厅点餐、遥控器等生活案例帮助理解 |
| 二、实现方式 | 以智能家居遥控器演示基本命令,以场景模式演示宏命令,以文本编辑器演示多步撤销与恢复 |
命令模式的核心优势:
- 解耦调用者与接收者:调用者只需知道命令接口,无需了解接收者的具体实现
- 易于扩展:新增命令只需新增命令类,无需修改调用者和其他命令,符合开闭原则
- 支持组合:通过宏命令将多个命令组合为复合命令,甚至支持嵌套
- 天然支持撤销/恢复:命令对象可保存状态,轻松实现多步撤销与恢复
- 支持命令队列与日志:命令对象可被序列化、持久化,实现任务调度和崩溃恢复
| 优点 | 缺点 |
|---|---|
| 调用者与接收者完全解耦 | 类数量膨胀------每个操作都需要一个命令类 |
| 易于扩展,新增命令无需修改已有代码 | 命令对象可能持有大量状态,内存开销较大 |
| 支持宏命令,可将命令组合与嵌套 | 撤销/恢复的实现需要命令对象保存前置状态 |
| 天然支持撤销/恢复、队列、日志 | 调用链变长,调试复杂度增加 |
| 命令对象可序列化,支持持久化与分布式 | 对于简单操作,引入命令模式可能过度设计 |
在实际开发中,命令模式广泛应用于:Java 线程池中的 Runnable / Callable(命令对象)、Swing 中的 Action 接口、Spring 的 JdbcTemplate 回调、消息队列中的任务封装、文本编辑器的撤销/恢复、IDE 的宏录制与回放等。掌握命令模式,能帮助你构建灵活、可扩展、支持高级操作管理能力的系统。