命令模式(Command Pattern)是一种行为型设计模式,它将请求(命令)封装成一个对象,从而允许参数化客户端对象以传递请求,也允许将请求排队、记录请求日志以及支持可撤销的操作。使得发出请求的责任和执行请求的责任分割开。这样两者间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用与管理。
结构
命令模式包含以下结构:
- 命令接口(Command):命令模式定义了一个命令接口,通常包括一个执行命令的方法(如
execute()
)。该接口可以具有多个方法,用于执行不同的操作。 - 具体命令类(Concrete Commands):具体命令类实现了命令接口,它封装了请求的具体操作。每个具体命令类都关联了一个接收者对象,负责执行请求。
- 接收者类(Receiver):接收者是真正执行命令操作的对象。它包含了实际的业务逻辑,命令对象只是将请求传递给接收者来执行实际的操作。
- 调用者类(Invoker):调用者是客户端代码,负责创建命令对象并将其发送给接收者来执行。调用者不需要知道命令的具体细节,只需将命令对象发送给接收者。
- 客户端(Client):客户端创建具体命令对象并将其与接收者关联,然后将命令对象传递给调用者来触发执行。
示例
假设需要构建一个遥控器,该遥控器可以控制不同的电器(如电灯、音响等),并具备撤销功能。
首先定义一个命令接口 Command
,其中包含了执行命令和撤销命令的方法:
public interface Command {
void execute();
void undo();
}
然后,创建具体的命令类,例如 LightOnCommand
和 LightOffCommand
,它们分别代表打开和关闭电灯的命令:
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOn();
}
public void undo() {
light.turnOff();
}
}
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
public void execute() {
light.turnOff();
}
public void undo() {
light.turnOn();
}
}
接下来,再创建一个接收者类 Light
,它代表电灯,负责执行实际的操作:
public class Light {
public void turnOn() {
System.out.println("Light is on");
}
public void turnOff() {
System.out.println("Light is off");
}
}
然后,创建一个遥控器类 RemoteControl
,它可以存储命令并执行它们:
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
public void pressUndo() {
command.undo();
}
}
最后在客户端代码中使用这些类来模拟遥控器的操作:
public class Client {
public static void main(String[] args) {
// 创建电灯和命令对象
Light light = new Light();
Command lightOn = new LightOnCommand(light);
Command lightOff = new LightOffCommand(light);
// 创建遥控器并设置命令
RemoteControl remote = new RemoteControl();
remote.setCommand(lightOn);
// 按下遥控器按钮,打开电灯
remote.pressButton();
// 按下撤销按钮,关闭电灯
remote.pressUndo();
// 设置命令为关闭电灯
remote.setCommand(lightOff);
// 按下遥控器按钮,关闭电灯
remote.pressButton();
// 按下撤销按钮,再次打开电灯
remote.pressUndo();
}
}
在上面的示例中,使用命令模式来实现了遥控器控制电灯的功能,同时支持撤销操作。通过将命令封装成对象,可以灵活地控制不同的电器和命令,而不需要修改遥控器或电器的代码。这演示了命令模式的核心思想,即将请求封装成对象,使得可以参数化客户端对象,并支持撤销和重做操作。
优点
- 解耦发起者和接收者: 命令模式通过将请求封装成命令对象,解耦了命令的发起者(客户端)和接收者(实际执行命令的对象)。这意味着发起者不需要知道接收者的具体实现细节,从而降低了系统的耦合度。
- 支持撤销和重做: 命令对象可以记录命令的历史状态,因此支持撤销和重做操作。这对于实现事务和提供用户友好的撤销功能非常有用。
- 灵活性和可扩展性: 命令模式允许轻松地添加新的命令类,而无需修改现有的客户端代码。这提高了系统的灵活性和可扩展性。
- 命令排队和任务调度: 可以使用命令模式来构建命令队列,从而实现命令的排队和任务调度。这对于构建任务调度器或批处理处理系统非常有用。
- 可重用的命令: 由于命令对象是独立的,可以轻松地将它们组合成新的复合命令,从而实现更复杂的操作。
- 宏命令:可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即为宏命令。
缺点
- 类数量增加: 使用命令模式可能导致类的数量增加,因为每个命令通常需要一个具体命令类。这可能会增加代码的复杂性,特别是对于小型项目而言。
- 内存占用: 如果有大量的命令对象存在,可能会占用较多的内存空间。这需要在设计中进行权衡,以确保不会浪费过多的资源。
- 额外的复杂性: 在某些情况下,引入命令模式可能会引入不必要的复杂性,特别是对于简单的系统而言。只有在需要支持撤销、重做、任务排队等功能时才有意义。
适用场景
命令模式适用于以下场景:
- 支持撤销和重做操作: 当需要为应用程序实现撤销和重做功能时,命令模式是一个非常有用的选择。通过将每个操作封装成命令对象,并记录它们的历史状态,可以轻松地实现这些功能。
- 支持命令队列和任务调度: 当需要构建命令队列或任务调度器时,命令模式可以派上用场。可以将命令对象排队并按顺序执行,或者根据需要对它们进行调度。
- 解耦发起者和接收者: 当希望将命令的发起者和接收者之间解耦,以便降低系统的耦合度时,命令模式非常有用。发起者只需知道如何发送命令,而不需要知道接收者的实际操作。
- 日志和审计记录: 命令模式可以用于记录系统的操作日志或审计记录。通过将每个操作封装成命令对象,并记录它们的执行,可以轻松地跟踪系统的历史操作。
- 遥控器和按钮: 在遥控器应用程序中,命令模式常常用于将按钮与不同的电器设备(如电视、音响、灯光)关联起来。每个按钮对应一个命令,用户按下按钮后执行相应的命令。
- 图形用户界面(GUI): 命令模式在 GUI 开发中广泛使用。例如,菜单项、工具栏按钮和快捷键可以与命令对象相关联,用户操作触发执行相应的命令。
- 事务处理: 当需要实现事务处理,确保一系列操作要么全部成功要么全部失败时,命令模式可以帮助将每个操作封装成命令,并在需要时执行它们。
- 分布式系统通信: 在分布式系统中,可以使用命令模式来封装远程方法调用请求。命令对象可以包含远程调用的细节,以便在不同系统之间通信。
命令模式在实际应用中广泛用于日常编程,特别是在构建用户界面、事务处理系统、系统监控和远程通信等方面。
源码解析
java.lang.Runnable
接口的使用可以被视为一种命令模式的实现。虽然 Runnable
接口通常用于多线程编程,但它的设计和使用方式与命令模式的核心思想非常相似。
在 JDK 中,Runnable
接口是一个功能性接口(Functional Interface),它只包含一个抽象方法 run()
,该方法定义了需要在单独的线程中执行的代码逻辑。具体的任务可以通过实现 run()
方法并传递给 Thread
对象来运行。
让我们来看一个具体的示例,结合 JDK 源码中的使用情况:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("Task is running in a separate thread.");
}
}
public class Main {
public static void main(String[] args) {
// 创建任务对象
Runnable myRunnable = new MyRunnable();
// 创建线程并将任务分配给线程
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
在上述示例中,MyRunnable
类实现了 Runnable
接口,并在其 run()
方法中封装了具体的任务逻辑。然后,创建了一个 Thread
对象,并将 myRunnable
对象传递给它,最后启动线程。当线程运行时,它将调用 myRunnable
中的 run()
方法来执行任务。
这里的关键点是,Runnable
接口充当了命令模式中的命令接口,定义了执行任务的方法。Thread
对象充当了命令模式中的命令的调用者,负责执行命令。
在 JDK 中更高级用法中,Executor
框架也使用了类似的思想。Executor
框架允许将任务(实现了 Runnable
或 Callable
接口的对象)提交给执行器,以异步执行任务。这里的执行器充当了命令模式中的调用者,而任务对象则充当了命令。
总之,Runnable
接口在 JDK 中的使用展示了命令模式的核心思想,即将操作封装成对象,以支持异步执行,并提供了一种标准的方式来执行任务。