设计模式是软件开发中的经典智慧,命令模式作为行为型模式的代表,为我们提供了一种优雅的方式来封装请求、解耦调用者与接收者。本文将从概念到实践,全面解析Java中的命令模式。
一、引言:为什么要学习命令模式?
1.1 什么是命令模式?
**命令模式(Command Pattern)**是一种行为型设计模式,它将一个请求封装为一个对象,从而允许用户使用不同的请求对客户进行参数化、对请求排队或记录请求日志,以及支持可撤销的操作。
核心思想:将"请求的发起者"和"请求的执行者"解耦,通过引入中间的命令对象来实现灵活的调用。
1.2 学习价值
- 解耦优势:让调用者无需知道接收者的具体实现
- 扩展性强:新增命令无需修改现有代码
- 支持高级功能:轻松实现撤销、重做、队列、日志等
- 提升代码可维护性:职责清晰,符合单一职责原则
二、命令模式的结构与角色
2.1 四大核心角色
┌─────────────────────────────────────────────────────────┐
│ 调用者 (Invoker) │
│ - 持有命令对象 │
│ - 负责调用命令的执行方法 │
└───────────────────┬─────────────────────────────────────┘
│
│ 持有
▼
┌─────────────────────────────────────────────────────────┐
│ 命令接口 (Command) │
│ - 声明执行方法:execute() │
└───────────────────┬─────────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ 具体命令A │ │ 具体命令B │
│ ConcreteCmdA │ │ ConcreteCmdB │
└───────┬───────┘ └───────┬───────┘
│ │
│ 持有 │ 持有
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ 接收者 (Receiver) │
│ - 真正执行业务逻辑的对象 │
└─────────────────────────────────────────────────────────┘
2.2 角色详解
| 角色 | 职责 | 示例 |
|---|---|---|
| Command(命令接口) | 声明执行操作的抽象接口 | interface Command { void execute(); } |
| ConcreteCommand(具体命令) | 实现命令接口,绑定接收者 | LightOnCommand, LightOffCommand |
| Invoker(调用者) | 持有命令对象并调用执行 | RemoteControl |
| Receiver(接收者) | 执行实际业务逻辑 | Light, TV |
2.3 UML类图
┌─────────────────┐
│ <<interface>> │
│ Command │
├─────────────────┤
│ +execute(): void│
└────────┬────────┘
│
│ implements
▼
┌──────────────────────────────┐
│ ConcreteCommand │
├──────────────────────────────┤
│ -receiver: Receiver │
├──────────────────────────────┤
│ +ConcreteCommand(receiver) │
│ +execute(): void │
└─────────┬────────────────────┘
│
│ holds
▼
┌──────────────────────────────┐
│ Receiver │
├──────────────────────────────┤
│ +action(): void │
└──────────────────────────────┘
┌──────────────────────────────┐
│ Invoker │
├──────────────────────────────┤
│ -command: Command │
├──────────────────────────────┤
│ +setCommand(cmd: Command) │
│ +invoke(): void │
└──────────────────────────────┘
三、实现步骤:智能家居遥控器案例
3.1 场景说明
我们以"智能家居遥控器"为例:遥控器可以控制电灯、电视等家电的开/关操作。
3.2 第一步:定义命令接口
java
/**
* 命令接口:所有具体命令的抽象
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 撤销命令(可选功能)
*/
void undo();
}
3.3 第二步:创建接收者
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 + " 的灯已关闭");
}
}
/**
* 接收者:电视
*/
public class TV {
private 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 setInputChannel(int channel) {
System.out.println(location + " 的电视已切换到频道 " + channel);
}
}
3.4 第三步:实现具体命令
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 TVOnCommand implements Command {
private TV tv;
private int previousChannel;
public TVOnCommand(TV tv) {
this.tv = tv;
this.previousChannel = 1; // 默认频道
}
@Override
public void execute() {
tv.on();
tv.setInputChannel(previousChannel);
}
@Override
public void undo() {
tv.off();
}
}
3.5 第四步:创建调用者
java
/**
* 调用者:遥控器
* 支持多个按钮,每个按钮可以绑定不同的命令
*/
public class RemoteControl {
private Command[] onCommands;
private Command[] offCommands;
private Command undoCommand; // 用于撤销操作
/**
* 初始化遥控器,支持7个插槽
*/
public RemoteControl() {
onCommands = new Command[7];
offCommands = new Command[7];
// 初始化为空命令(避免空指针)
Command noCommand = new NoCommand();
for (int i = 0; i < 7; i++) {
onCommands[i] = noCommand;
offCommands[i] = noCommand;
}
undoCommand = noCommand;
}
/**
* 设置插槽的命令
*/
public void setCommand(int slot, Command onCommand, Command offCommand) {
onCommands[slot] = onCommand;
offCommands[slot] = offCommand;
}
/**
* 按下开按钮
*/
public void onButtonWasPushed(int slot) {
onCommands[slot].execute();
undoCommand = onCommands[slot];
}
/**
* 按下关按钮
*/
public void offButtonWasPushed(int slot) {
offCommands[slot].execute();
undoCommand = offCommands[slot];
}
/**
* 撤销上一次操作
*/
public void undoButtonWasPushed() {
undoCommand.undo();
}
/**
* 显示遥控器状态
*/
public void toStringDisplay() {
System.out.println("\n------ 遥控器状态 ------");
for (int i = 0; i < onCommands.length; i++) {
System.out.println("[插槽 " + i + "] " +
onCommands[i].getClass().getName() + " " +
offCommands[i].getClass().getName());
}
System.out.println("[撤销] " + undoCommand.getClass().getName());
}
}
/**
* 空命令:默认命令,什么也不做
* 用于初始化数组,避免空指针异常
*/
public class NoCommand implements Command {
@Override
public void execute() {
// 什么也不做
}
@Override
public void undo() {
// 什么也不做
}
}
3.6 第五步:客户端使用
java
/**
* 客户端:测试命令模式
*/
public class RemoteControlTest {
public static void main(String[] args) {
// 1. 创建接收者
Light livingRoomLight = new Light("客厅");
Light kitchenLight = new Light("厨房");
TV livingRoomTV = new TV("客厅");
// 2. 创建具体命令
LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
LightOnCommand kitchenLightOn = new LightOnCommand(kitchenLight);
LightOffCommand kitchenLightOff = new LightOffCommand(kitchenLight);
TVOnCommand livingRoomTVOn = new TVOnCommand(livingRoomTV);
// 3. 创建调用者(遥控器)并设置命令
RemoteControl remote = new RemoteControl();
remote.setCommand(0, livingRoomLightOn, livingRoomLightOff);
remote.setCommand(1, kitchenLightOn, kitchenLightOff);
remote.setCommand(2, livingRoomTVOn, livingRoomTVOn);
// 4. 显示遥控器状态
remote.toStringDisplay();
// 5. 测试功能
System.out.println("\n------ 测试客厅灯 ------");
remote.onButtonWasPushed(0);
remote.offButtonWasPushed(0);
System.out.println("\n------ 测试撤销 ------");
remote.onButtonWasPushed(0); // 打开灯
remote.undoButtonWasPushed(); // 撤销:关闭灯
System.out.println("\n------ 测试厨房灯 ------");
remote.onButtonWasPushed(1);
System.out.println("\n------ 测试电视 ------");
remote.onButtonWasPushed(2);
}
}
输出结果:
------ 遥控器状态 ------
[插槽 0] LightOnCommand LightOffCommand
[插槽 1] LightOnCommand LightOffCommand
[插槽 2] TVOnCommand TVOnCommand
[插槽 3] NoCommand NoCommand
[插槽 4] NoCommand NoCommand
[插槽 5] NoCommand NoCommand
[插槽 6] NoCommand NoCommand
[撤销] NoCommand
------ 测试客厅灯 ------
客厅 的灯已打开
客厅 的灯已关闭
------ 测试撤销 ------
客厅 的灯已打开
客厅 的灯已关闭
------ 测试厨房灯 ------
厨房 的灯已打开
------ 测试电视 ------
客厅 的电视已打开
客厅 的电视已切换到频道 1
四、优缺点分析
4.1 主要优势
| 优势 | 说明 |
|---|---|
| 解耦 | 调用者与接收者完全解耦,互不依赖 |
| 扩展性强 | 新增命令只需实现Command接口,符合开闭原则 |
| 支持撤销/重做 | 通过保存命令状态,轻松实现撤销操作 |
| 组合操作 | 可将多个命令组合为宏命令(Composite模式) |
| 延迟执行 | 命令可以在需要的时刻才执行 |
| 日志与排队 | 方便记录操作日志或实现任务队列 |
4.2 潜在缺点
| 缺点 | 说明 | 缓解方案 |
|---|---|---|
| 类数量增加 | 每个具体命令都是一个类 | 使用Lambda表达式(Java 8+)或匿名内部类 |
| 系统复杂度 | 引入额外抽象层,理解成本增加 | 明确文档注释,合理使用场景 |
| 内存开销 | 大量命令对象可能占用内存 | 使用命令池或享元模式优化 |
五、应用场景与实战案例
5.1 典型应用场景
- GUI事件处理:按钮点击、菜单选择等
- 事务管理:数据库操作的提交与回滚
- 任务队列与调度:后台任务异步执行
- 日志记录:记录操作历史用于审计
- 宏命令:批量执行一系列操作
- 多级撤销/重做:编辑器等应用的核心功能
5.2 Java标准库中的应用
1. java.lang.Runnable
java
// Runnable是最简单的命令模式应用
public interface Runnable {
public abstract void run();
}
// 使用示例
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("执行任务");
}
});
thread.start();
2. java.swing.Action
Swing中的Action接口封装了按钮的行为和状态:
java
Action saveAction = new AbstractAction("保存", saveIcon) {
@Override
public void actionPerformed(ActionEvent e) {
// 执行保存操作
saveDocument();
}
};
5.3 框架中的应用
Spring框架:事务管理
java
@Transactional
public void transferMoney(Account from, Account to, double amount) {
// Spring通过命令模式封装事务操作
from.debit(amount);
to.credit(amount);
}
Struts/Hibernate:Action与拦截器
这些框架使用命令模式将HTTP请求封装为Action对象,由框架统一调度。
六、与其他模式的对比
6.1 命令模式 vs 策略模式
| 对比维度 | 命令模式 | 策略模式 |
|---|---|---|
| 目的 | 封装请求,支持撤销/重做 | 封装算法, interchangeable |
| 关注点 | 操作的执行过程 | 算法的不同实现 |
| 状态 | 可保存执行状态 | 通常无状态 |
| 使用场景 | GUI操作、事务、宏命令 | 算法替换、业务规则变化 |
代码对比:
java
// 策略模式:关注算法本身
public interface SortStrategy {
void sort(int[] array);
}
// 命令模式:关注操作的封装与执行
public interface Command {
void execute();
void undo();
}
6.2 命令模式 vs 状态模式
| 对比维度 | 命令模式 | 状态模式 |
|---|---|---|
| 变化维度 | 操作的变化 | 状态的变化 |
| 行为触发 | 调用者主动调用 | 状态内部自动切换 |
| 适用场景 | 解耦请求与处理 | 对象行为随状态变化 |
6.3 命令模式 vs 责任链模式
- 命令模式:命令被明确接收者执行
- 责任链模式:请求沿链传递,直到被处理
七、总结与最佳实践
7.1 核心价值提炼
命令模式的本质是**"将请求封装为对象"**,通过这一简单思想实现了:
- ✅ 调用者与接收者的彻底解耦
- ✅ 操作的可记录、可撤销、可延迟执行
- ✅ 系统的高扩展性和灵活性
7.2 使用建议
✅ 推荐使用的场景:
- 需要支持撤销/重做操作
- 需要将请求排队或异步执行
- GUI事件处理框架
- 事务管理系统
- 需要组合多个操作
❌ 不建议使用的场景:
- 简单的一次性操作
- 命令数量极少且不会扩展
- 对性能要求极高的场景
7.3 优化技巧
java
// Java 8+:使用Lambda简化命令
remote.setCommand(0,
() -> light.on(),
() -> light.off()
);
// 宏命令:组合多个命令
public class MacroCommand implements Command {
private Command[] commands;
public MacroCommand(Command[] commands) {
this.commands = commands;
}
@Override
public void execute() {
for (Command cmd : commands) {
cmd.execute();
}
}
}
最后的话:设计模式不是僵化的教条,而是解决问题的智慧。在实际项目中,根据业务需求灵活运用命令模式,才能真正发挥它的价值。