深入浅出设计模式【十四、命令模式】
一、命令模式介绍
命令模式的核心思想是将请求(或操作)封装为独立的对象。这个对象包含了执行该请求所需的所有信息(接收者、方法、参数)。
通过这种封装,命令的发出者(Invoker)和命令的执行者(Receiver)被彻底解耦。发出者不需要知道执行者是谁、执行了什么操作、以及操作如何实现。它只需要知道如何触发命令对象即可。这使得请求本身可以像其他对象一样被存储、传递、排队、记录和撤销,从而为构建灵活且功能丰富的系统奠定了基础。
二、核心概念与意图
-
核心概念:
- 命令 (Command): 声明执行操作的接口,通常包含一个
execute()方法。 - 具体命令 (Concrete Command): 实现命令接口。它通常持有对一个接收者对象的引用,并将调用委托给接收者的一个或多个方法。它定义了操作和接收者之间的绑定关系。
- 客户端 (Client): 创建具体命令对象,并为其配置接收者(即,确定命令对象在
execute()时应该调用哪个接收者的哪个方法)。 - 调用者 (Invoker): 要求命令对象执行请求。它持有命令对象,并在某个时间点(如按钮被点击、定时器触发)调用命令对象的
execute()方法。 - 接收者 (Receiver): 知道如何执行与请求相关的操作。任何类都可以作为接收者。具体命令对象会将调用委托给它。
- 命令 (Command): 声明执行操作的接口,通常包含一个
-
意图:
- 将一个请求封装为一个对象,从而使您可以用不同的请求对客户进行参数化。
- 对请求排队或记录请求日志,以及支持可撤销的操作。
- 将调用操作的对象与知道如何实现该操作的对象解耦。
三、适用场景剖析
命令模式在以下场景中非常有效:
- 需要将操作作为参数进行传递时: 例如,需要将用户界面的一个操作(如点击按钮)配置为执行某个业务逻辑。命令对象可以完美地作为这个“操作”的载体。
- 需要支持操作的撤销 (Undo) 和重做 (Redo) 时: 这是命令模式的杀手级应用。通过存储已执行的命令列表,并在命令对象中实现
undo()方法(通常与execute()逻辑相反),可以轻松实现历史记录和回滚功能。 - 需要支持事务(Transaction)语义时: 需要将一系列操作作为一个原子单元来执行。如果其中某个操作失败,可以回滚之前所有已执行的操作。命令对象可以记录所有操作,并在失败时触发一系列
undo()操作。 - 需要将请求排队、调度执行或记录日志时: 命令对象可以被放入队列中,由工作线程按顺序执行(如线程池、任务队列)。也可以在被执行前记录日志,用于系统审计或故障恢复。
- 需要支持宏命令(Macro Command)时: 即用一个命令代表一系列其他命令的组合(组合模式 + 命令模式)。
四、UML 类图解析(Mermaid)
以下UML类图清晰地展示了命令模式的结构和角色间的关系:
Command(命令接口): 声明执行操作的接口,通常是execute()方法。为了实现撤销,通常还会包含undo()方法。ConcreteCommand(具体命令):- 实现命令接口。
- 持有对一个接收者对象的引用 (
-receiver: Receiver)。 - 在
execute()方法中,调用接收者的一个或多个方法(如receiver.action())来完成具体的业务逻辑。 - 可能存储执行前的状态 (
-state),以便在undo()方法中能够将接收者恢复到执行前的状态。
Invoker(调用者):- 持有命令对象 (
-command: Command)。 - 提供设置命令的方法 (
setCommand())。 - 在特定时机(如
actionPerformed())调用命令对象的execute()方法 (executeCommand())。 - 它不知道命令的具体内容,只负责触发。
- 持有命令对象 (
Receiver(接收者):- 知道如何执行具体的操作,包含实际的业务逻辑(
action()方法)。 - 具体命令对象会将调用委托给接收者。
- 知道如何执行具体的操作,包含实际的业务逻辑(
Client(客户端):- 创建具体命令对象 (
ConcreteCommand)。 - 为命令对象配置接收者 (
receiver)。 - 将命令对象传递给调用者 (
invoker.setCommand(command))。
- 创建具体命令对象 (
五、各种实现方式及其优缺点
命令模式的实现关键在于如何设计命令接口和管理命令的生命周期。
1. 标准实现(接口 + 类)
即上述UML所描述的方式,为每个操作定义一个具体的命令类。
// 1. Command Interface
public interface Command {
void execute();
void undo(); // For undo functionality
}
// 2. Receiver
public class Light {
public void on() {
System.out.println("Light is ON");
}
public void off() {
System.out.println("Light is OFF");
}
}
// 3. Concrete Command
public class LightOnCommand implements Command {
private Light light; // The receiver
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.on(); // Delegate to the receiver
}
@Override
public void undo() {
light.off(); // The undo action is the opposite
}
}
// 4. Invoker (e.g., a remote control with one button)
public class SimpleRemoteControl {
private Command slot; // Holds one command
public void setCommand(Command command) {
this.slot = command;
}
public void buttonWasPressed() {
slot.execute(); // Executes the currently set command
}
}
// 5. Client
public class Client {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl(); // Invoker
Light light = new Light(); // Receiver
LightOnCommand lightOn = new LightOnCommand(light); // ConcreteCommand
remote.setCommand(lightOn); // Client configures the invoker with a command
remote.buttonWasPressed(); // Invoker triggers the command
}
}
- 优点:
- 解耦彻底: 调用者与接收者完全解耦。
- 易于扩展: 添加新命令只需实现新的具体命令类,符合开闭原则。
- 功能强大: 天然支持宏命令、队列、日志、撤销/重做等高级功能。
- 缺点:
- 类爆炸 (Class Bloat): 如果系统有大量操作,会产生许多具体命令类,增加系统复杂度。
2. 函数式命令(利用Lambda表达式或方法引用)
在Java 8+中,如果命令接口只包含一个方法(通常是 execute()),可以利用函数式接口和Lambda表达式来简化实现,避免为每个命令创建单独的类。
// Command is now a Functional Interface
@FunctionalInterface
public interface Command {
void execute();
}
// Receiver remains the same
public class Light {
public void on() { System.out.println("Light is ON"); }
public void off() { System.out.println("Light is OFF"); }
}
// Invoker remains the same
public class SimpleRemoteControl {
private Command command;
public void setCommand(Command command) { this.command = command; }
public void buttonWasPressed() { command.execute(); }
}
// Client uses Lambda expressions or method references
public class Client {
public static void main(String[] args) {
SimpleRemoteControl remote = new SimpleRemoteControl();
Light light = new Light();
// Instead of creating a concrete class, use a lambda
remote.setCommand(() -> light.on()); // Set command to turn light on
remote.buttonWasPressed();
// Can also use method references if the signature matches
remote.setCommand(light::off); // Set command to turn light off
remote.buttonWasPressed();
}
}
- 优点:
- 代码简洁: 极大地减少了样板代码,避免了类爆炸问题。
- 灵活直观: 在客户端就地定义行为,非常直观。
- 缺点:
- 局限性: 难以实现复杂的命令(如需要存储状态以实现
undo())。undo()功能很难用简单的Lambda实现,除非引入更复杂的结构。 - 可读性: 复杂的逻辑写在Lambda中可能降低可读性。
- 局限性: 难以实现复杂的命令(如需要存储状态以实现
选择建议: 对于简单的、不需要撤销功能的命令,优先使用函数式实现。对于需要状态管理、撤销/重做、事务等复杂功能的命令,使用标准的类实现。
六、最佳实践
-
实现撤销 (Undo) 操作:
- 在
Command接口中添加undo()方法。 undo()的实现通常是execute()的逆操作。- 调用者(或一个专门的
History对象)需要维护一个已执行命令的栈(Stack)。执行命令时,将其压入栈。执行撤销时,弹出栈顶命令并调用其undo()方法。
public class RemoteControlWithUndo { private Command lastExecutedCommand; public void setAndExecuteCommand(Command command) { command.execute(); lastExecutedCommand = command; // Store for undo } public void undoButtonWasPressed() { if (lastExecutedCommand != null) { lastExecutedCommand.undo(); } } } - 在
-
实现宏命令 (Macro Command):
- 宏命令也是一个具体命令,但它包含一个命令列表。
- 其
execute()方法会遍历并执行列表中的所有命令。 - 其
undo()方法通常会以相反的顺序执行所有命令的undo()(注意:实现一个完全正确的宏撤销可能很复杂)。
public class MacroCommand implements Command { private List<Command> commands; public MacroCommand(List<Command> commands) { this.commands = commands; } @Override public void execute() { for (Command command : commands) { command.execute(); } } @Override public void undo() { // Undo in reverse order for (int i = commands.size() - 1; i >= 0; i--) { commands.get(i).undo(); } } } -
与备忘录模式 (Memento) 结合: 对于复杂的撤销操作,如果恢复状态很困难,可以让命令对象在执行前从接收者获取一个状态的备忘录(Memento),并在撤销时使用该备忘录来恢复状态。
-
空对象 (Null Object) 应用: 在调用者中,可以初始化一个什么都不做的空命令(
NoCommand),避免对null进行检查。public class NoCommand implements Command { @Override public void execute() { /* Do nothing */ } @Override public void undo() { /* Do nothing */ } } // In Invoker: // private Command slot = new NoCommand(); // Default to no command
七、在开发中的演变和应用
命令模式的思想是现代异步编程和系统架构的核心:
- 任务队列与线程池:
Runnable和Callable对象本质上就是命令对象。它们被提交到ExecutorService(调用者),由线程池中的线程(接收者)执行。这实现了任务的提交与执行的解耦。 - 消息队列与事件驱动架构: 发送到消息队列(如RabbitMQ、Kafka)中的消息可以看作是序列化的命令对象。消费者接收到消息后,将其反序列化并执行其中包含的命令。这是分布式环境下的命令模式。
- 事务脚本与工作单元: 在数据库事务中,一系列操作可以被组织成命令对象。如果所有操作成功,则提交事务;如果任何一个失败,则回滚所有操作(执行每个命令的补偿操作)。
- 异步操作与回调: 在GUI编程或网络编程中,将一个操作(命令)提交给后台线程执行,并在操作完成后执行另一个操作(回调命令),这是一种常见的模式。
八、真实开发案例(Java语言内部、知名开源框架、工具)
-
Java
Runnable接口:Runnable接口就是一个最经典的命令接口,它只定义了一个方法run()。Thread类(调用者)持有Runnable命令,并在调用start()后最终执行run()方法。ExecutorService是更高级的调用者,它管理着一个线程池来执行提交的Runnable或Callable命令。
-
Swing/AWT 的
Action接口:- Java Swing 中的
Action接口扩展了ActionListener,它是一个丰富的命令接口,不仅定义了actionPerformed()方法(即execute()),还包含了命令的文本、图标、启用状态等元信息。 JButton、JMenuItem等组件(调用者)可以设置一个Action命令。组件会自动根据Action的元信息来更新自己的显示状态。
- Java Swing 中的
-
Spring Framework 的
JdbcTemplate:- 虽然不完全是标准的命令模式,但其思想高度吻合。
JdbcTemplate的execute(ConnectionCallback),query(PreparedStatementCreator, RowMapper)等方法接受各种回调接口。 - 这些回调接口(如
ConnectionCallback,PreparedStatementCreator)就是命令接口,定义了如何在JDBC连接的上下文中执行操作。 - 开发者提供这些接口的具体实现(具体命令),
JdbcTemplate(调用者)负责管理连接、语句等资源,并在正确的时机调用这些命令。这完美地将可变的部分(SQL操作)与不变的部分(资源管理)分离开来。
- 虽然不完全是标准的命令模式,但其思想高度吻合。
-
Hibernate/JPA 的
@PostPersist,@PreUpdate等监听器:- 可以将这些注解标记的方法视为一种“命令”,Hibernate(调用者)在特定的生命周期事件(如持久化前、更新后)自动触发执行这些命令。
-
项目中的工作流引擎或审批系统:
- 每个审批操作(通过、驳回、转交)都可以封装成一个命令对象。工作流引擎(调用者)根据流程定义来执行相应的命令。
九、总结
| 方面 | 总结 |
|---|---|
| 模式类型 | 行为型设计模式 |
| 核心意图 | 将请求封装为对象,从而使您可以用不同的请求参数化客户端,并支持请求的排队、记录、撤销。 |
| 关键角色 | 命令(Command), 具体命令(ConcreteCommand), 调用者(Invoker), 接收者(Receiver), 客户端(Client) |
| 核心机制 | 封装与委托: 将请求细节封装在命令对象中。调用者触发命令,命令委托接收者执行操作。 |
| 主要优点 | 1. 解耦: 解耦了请求发送者和接收者。 2. 灵活性高: 新命令易扩展,命令可组合(宏命令)。 3. 支持高级功能: 天然支持撤销、事务、队列、日志。 |
| 主要缺点 | 1. 类爆炸: 可能产生大量具体的命令类(可用Lambda缓解)。 2. 复杂度增加: 引入新的抽象层。 |
| 适用场景 | 1. 需要支持撤销/重做、事务。 2. 需要将操作排队、记录日志、或远程执行。 3. 需要用不同操作参数化对象(如GUI按钮)。 4. 需要支持宏命令。 |
| 实现选择 | 标准类实现: 功能强大,支持复杂操作(撤销、状态)。 函数式实现 (Lambda): 简洁灵活,适用于简单命令。 |
| 最佳实践 | 使用空对象;与备忘录模式结合实现复杂撤销;利用宏命令组合操作。 |
| 现代应用 | 异步任务 (Runnable),消息队列,事务管理,事件驱动架构的理论基础。 |
| 真实案例 | Java Runnable/Callable (核心),Swing Action (GUI),Spring JdbcTemplate 回调 (资源管理)。 |
命令模式通过将“操作”提升为一等公民(对象),赋予了请求前所未有的灵活性和控制力。它是实现撤销、事务、任务队列等高级功能的基石,深刻影响着从GUI编程到分布式系统设计的方方面面。理解并掌握命令模式,是构建复杂、灵活、可维护系统的关键技能,它教会我们如何将“做什么”和“谁来做”、“何时做”有效地分离开来。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19120853

浙公网安备 33010602011771号