命令模式(Command Pattern)是一种行为型设计模式,它将请求封装为一个对象,从而使得请求的发送者和接收者解耦。
一、命令模式的核心思想
命令模式的核心思想是将“请求”封装成为一个对象,从而使得我们可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
二、命令模式的结构
在命令模式结构图中,通常包含以下几个角色:
- Command(抽象命令类):一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
- ConcreteCommand(具体命令类):抽象命令类的子类,实现了在抽象命令类中声明的方法。它对应具体的接收者对象,将接收者对象的动作绑定其中。
- Invoker(调用者):请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。
- Receiver(接收者):执行与请求相关的操作,它具体实现对请求的业务处理。
- Client(客户端):创建具体的命令对象,并且设置命令对象的接收者。这里的客户端并不是常规意义上的客户端,而是在组装命令对象和接收者,也可以将其称为装配者。
三、命令模式的实现
命令模式的实现通常涉及以下几个步骤:
- 定义一个抽象的Command接口,其中声明一个execute()方法。
- 具体的命令类(ConcreteCommand)实现这个execute()方法,将一个发送者(Invoker)的请求转换为一个或多个接收者(Receiver)的操作。
- 发送者(Invoker)并不知道接收者的具体实现,它只需要触发命令对象的execute()方法即可。
四、命令模式的应用场景
命令模式有广泛的应用场景,主要包括以下几种:
- 将各种请求以命令的形式加入到队列中,然后由相应的线程或进程逐个执行这些命令。
- 将一系列相关的操作封装成一个命令,要么全部执行成功,要么全部回滚。
- 通过保存历史命令,可以轻松地实现撤销和重做功能。
- 在执行命令的时候,可以同时将该命令记录到日志中,用于审计跟踪。
- 命令对象可以实现一个undo()方法,用于撤销之前执行的命令。
- 将多个命令组合成一个复合命令,可以一次性执行一系列操作。
- 在GUI程序中,可以使用命令模式来发出和处理用户点击按钮时表示的操作。
- 在多线程操作中,命令模式可以帮助程序在后台自动发出指令并处理其他业务,而不用等待线程完成操作。
五、命令模式的优点
- 降低了系统的耦合度:由于请求者与接收者之间通过命令对象进行交互,因此它们之间的依赖关系被解耦。
- 增强了系统的扩展性:新的命令可以很容易地添加到系统中,而不需要修改现有的客户端代码。
- 提供了命令的撤销和重做功能:通过保存历史命令,可以轻松地实现撤销和重做功能。
- 实现了请求的排队和日志记录:命令对象可以被存储在队列中,并在适当的时候执行。同时,也可以在执行命令的时候记录日志。
六、命令模式的缺点
- 增加了系统的复杂性:由于引入了抽象命令类、具体命令类、调用者和接收者等多个角色,系统的复杂性可能会增加。
- 可能会导致过度设计:如果系统中的命令数量并不多,或者命令的执行逻辑比较简单,那么使用命令模式可能会导致过度设计。
七、实战流程
不使用命令模式
public class Editor {
public void copy() {
// 复制逻辑
}
public void paste() {
// 粘贴逻辑
}
public void undo() {
// 撤销逻辑
}
}
public class User {
public static void main(String[] args) {
Editor editor = new Editor();
editor.copy();
editor.paste();
editor.undo();
}
}
使用命令模式优化
/**
* 接收者
*/
public class Editor {
public void copy() {
System.out.println("复制内容");
}
public void paste() {
System.out.println("粘贴内容");
}
public void undo() {
System.out.println("撤销操作");
}
}
/**
* 命令接口
*/
public interface Command {
void execute();
}
/**
* 具体命令类
*/
public class CopyCommand implements Command {
private Editor editor;
public CopyCommand(Editor editor) {
this.editor = editor;
}
@Override
public void execute() {
editor.copy();
}
}
public class PasteCommand implements Command {
private Editor editor;
public PasteCommand(Editor editor) {
this.editor = editor;
}
@Override
public void execute() {
editor.paste();
}
}
public class UndoCommand implements Command {
private Editor editor;
public UndoCommand(Editor editor) {
this.editor = editor;
}
@Override
public void execute() {
editor.undo();
}
}
/**
* 调用者
*/
public class Menu {
private Command copyCommand;
private Command pasteCommand;
private Command undoCommand;
public Menu(Command copyCommand, Command pasteCommand, Command undoCommand) {
this.copyCommand = copyCommand;
this.pasteCommand = pasteCommand;
this.undoCommand = undoCommand;
}
public void clickCopy() {
copyCommand.execute();
}
public void clickPaste() {
pasteCommand.execute();
}
public void clickUndo() {
undoCommand.execute();
}
}
public class CommandTest {
public static void main(String[] args) {
Editor editor = new Editor();
Command copy = new CopyCommand(editor);
Command paste = new PasteCommand(editor);
Command undo = new UndoCommand(editor);
Menu menu = new Menu(copy, paste, undo);
menu.clickCopy();
menu.clickPaste();
menu.clickUndo();
}
}
八、在JDK源码中应用命令模式的例子
1、java.lang.Runnable 和 java.util.concurrent.Callable
Runnable 和 Callable 接口是命令模式的典型例子。这两个接口封装了要在另一个线程中执行的任务,具体任务内容由 run() 方法或 call() 方法来实现,线程池或者线程的调用者在需要的时候执行这些任务。
(1)命令模式结构
- 命令接口:Runnable 和 Callable 是命令接口,定义了 run() 和 call() 方法,表示要执行的命令。
- 具体命令:任何实现 Runnable 或 Callable 的类都是具体命令类,封装了实际的任务逻辑。
- 调用者:Thread 或线程池(如 ExecutorService),负责触发命令的执行。
Runnable 的 run() 方法是命令的执行逻辑,而 Thread 是调用者,它负责在合适的时间触发 run() 的执行。
(2)代码示例
public class CommandExample {
public static void main(String[] args) {
// 创建一个命令(Runnable任务)
Runnable command = new Runnable() {
@Override
public void run() {
System.out.println("执行命令: 线程中的任务");
}
};
// 调用者:Thread 执行命令
Thread thread = new Thread(command);
thread.start();
}
}
2、javax.swing.Action
在 Swing 框架中,Action 接口用于封装行为,它是典型的命令模式。Action 可以被 JButton、JMenuItem 等组件复用,从而将动作的定义与其使用解耦。
(1)命令模式结构
- 命令接口:Action 接口。
- 具体命令:实现 Action 接口的具体动作类,定义 actionPerformed(ActionEvent e) 方法,封装了执行逻辑。
- 调用者:Swing 组件,如 JButton 或 JMenuItem,这些组件触发 Action 的执行。
(2)代码示例
sayHelloAction 是命令,JButton 是调用者,点击按钮会触发 actionPerformed 方法的执行。
import javax.swing.*;
import java.awt.event.ActionEvent;
public class SwingCommandExample {
public static void main(String[] args) {
// 创建一个具体命令(Action)
Action sayHelloAction = new AbstractAction("Say Hello") {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Hello, World!");
}
};
// 调用者:JButton,触发命令的执行
JButton button = new JButton(sayHelloAction);
// 创建一个简单的Swing窗口
JFrame frame = new JFrame("Command Pattern Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(button);
frame.pack();
frame.setVisible(true);
}
}
3、java.util.Timer 和 java.util.TimerTask
TimerTask 是 JDK 中另一个典型的命令模式实现。TimerTask 类实现了 Runnable 接口,其 run() 方法封装了定时要执行的任务,Timer 类负责调度这些任务的执行。
(1)命令模式结构
- 命令接口:TimerTask 是具体命令类,封装了任务逻辑。
- 调用者:Timer,负责在指定时间触发命令。
(2)代码示例
TimerTask 的 run() 方法是封装的任务逻辑,Timer 是负责触发任务执行的调用者。
import java.util.Timer;
import java.util.TimerTask;
public class TimerCommandExample {
public static void main(String[] args) {
// 创建一个定时器
Timer timer = new Timer();
// 创建一个具体命令(TimerTask)
TimerTask task = new TimerTask() {
@Override
public void run() {
System.out.println("执行定时任务");
}
};
// 调用者:Timer,调度任务
timer.schedule(task, 1000); // 1秒后执行任务
}
}