1 基本介绍
命令模式(Command Pattern)是一种 行为型 设计模式,它 用对象来代表实际行动,将命令封装到一个对象中,从而实现 命令发布者 与 命令实现者 之间的解耦。
2 案例
本案例实现了让机器小车按照下图的路线移动:
注意两点:
- x , y x, y x,y 轴和数学中的 x , y x, y x,y 轴不同。
- 1️⃣2️⃣3️⃣4️⃣ 代表小车的位置,小车从 1️⃣ 开始移动,途径 2️⃣3️⃣4️⃣,最终从 4️⃣ 返回 1️⃣。
2.1 Instruction 接口
public interface Instruction { // 指令
void execute(); // 执行具体的指令
}
2.2 AdvanceInstruction 类
public class AdvanceInstruction implements Instruction { // 前进指令
private Robot robot; // 机器小车
private int distance; // 前进的距离
public AdvanceInstruction(Robot robot, int distance) {
this.robot = robot;
this.distance = distance;
}
@Override
public void execute() {
robot.advance(distance);
}
}
2.3 TurnLeftInstruction 类
public class TurnLeftInstruction implements Instruction { // 左转指令
private Robot robot; // 机器小车
public TurnLeftInstruction(Robot robot) {
this.robot = robot;
}
@Override
public void execute() {
robot.turnLeft();
}
}
2.4 TurnRightInstruction 类
public class TurnRightInstruction implements Instruction { // 右转指令
private Robot robot; // 机器小车
public TurnRightInstruction(Robot robot) {
this.robot = robot;
}
@Override
public void execute() {
robot.turnRight();
}
}
2.5 MacroInstruction 类
import java.util.ArrayList;
import java.util.List;
public class MacroInstruction implements Instruction { // 宏指令,一条指令 包含了 多条指令
private List<Instruction> instructions = new ArrayList<>(); // 指令集合
@Override
public void execute() { // 执行集合中的所有指令
for (Instruction instruction : instructions) {
instruction.execute();
}
}
public void append(Instruction instruction) { // 追加一条新指令
if (instruction != this) { // 防止将自身添加到指令集合中
instructions.add(instruction);
}
}
public void delete() { // 删除最后一条指令
if (!instructions.isEmpty()) {
instructions.remove(instructions.size() - 1);
}
}
public void clear() { // 清空所有指令
instructions.clear();
}
}
2.6 Robot 类
public class Robot { // 机器小车,可以把机器人想像成实验室的 小车
private int x; // 横坐标,初始值为 0
private int y; // 纵坐标,初始值为 0
// 方向有四个值 [0, 1, 2, 3],分别为 向右、向下、向左、向上
private int dir; // 当前的方向,初始方向为向右
public Robot() { // 初始化小车时打印小车的位置和方向
printPosition();
printDirection();
}
public void advance(int distance) { // 沿当前方向移动指定的距离
switch (dir) {
case 0: // 向右
x += distance;
break;
case 1: // 向下
y += distance;
break;
case 2: // 向左
x -= distance;
break;
case 3: // 向上
y -= distance;
break;
}
// 模拟小车移动所需的时间
try {
Thread.sleep(distance * 10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
printPosition(); // 打印当前的坐标
}
public void turnLeft() { // 向左转向
dir = (dir + 3) % 4;
// 模拟小车转向所需的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
printDirection(); // 打印当前的方向
}
public void turnRight() { // 向右转向
dir = (dir + 1) % 4;
// 模拟小车转向所需的时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
printDirection(); // 打印当前的方向
}
public void printPosition() { // 打印小车当前的坐标
System.out.println("小车现在的「坐标」为 (" + x + ", " + y + ")");
}
public void printDirection() { // 打印小车当前的方向
String direction = null;
switch (dir) {
case 0: // 向右
direction = "向右";
break;
case 1: // 向下
direction = "向下";
break;
case 2: // 向左
direction = "向左";
break;
case 3: // 向上
direction = "向上";
break;
}
System.out.println("小车现在的「方向」为 " + direction);
}
}
2.7 Controller 类
public class Controller { // 控制机器小车行为的控制器
// 存储小车以前执行过命令的 历史指令集
private MacroInstruction history = new MacroInstruction();
public void execute(Instruction instruction) { // 执行指令
history.append(instruction); // 将指令存储到 历史指令集 中
instruction.execute();
}
public void repeat(int times) { // 重复指定次数之前执行的所有命令
for (int i = 0; i < times; i++) {
history.execute();
}
}
}
2.8 Client 类
public class Client { // 客户端,测试了 用控制器让小车沿如图所示的正方形移动
public static void main(String[] args) {
Robot robot = new Robot();
Controller controller = new Controller();
controller.execute(new AdvanceInstruction(robot, 100)); // 执行前进命令
controller.execute(new TurnRightInstruction(robot)); // 执行右转命令
controller.repeat(3);
}
}
2.9 Client 类的运行结果
小车现在的「坐标」为 (0, 0)
小车现在的「方向」为 向右
小车现在的「坐标」为 (100, 0)
小车现在的「方向」为 向下
小车现在的「坐标」为 (100, 100)
小车现在的「方向」为 向左
小车现在的「坐标」为 (0, 100)
小车现在的「方向」为 向上
小车现在的「坐标」为 (0, 0)
小车现在的「方向」为 向右
2.10 总结
本案例没有使用到小车的所有行为,只是挑选了一部分进行实现,这是为了避免案例太过复杂,如果对本案例的小车很感兴趣,还可以在 Controller
类中实现 移除 和 清空 的方法,即 删除历史记录中的最后一条指令 和 清空历史记录。
本案例将 命令 封装到 Instruction
接口的实现类的对象实例中,从而将 命令发布者 Client
类 和 命令实现者 Robot
类解耦,可以发现,这样做可以通过 Controller
类将某些命令重复执行多次(还可以实现 移除 方法来将某些命令从历史记录中删除),这就是 命令模式 的优点。
3 各角色之间的关系
3.1 角色
3.1.1 Command ( 命令 )
该角色负责 定义 执行命令的 接口 execute()
。本案例中,Instruction
接口扮演了该角色。
3.1.2 ConcreteCommand ( 具体命令 )
该角色负责 实现 Command 角色定义的 接口。本案例中,AdvanceInstruction, TurnLeftInstruction, TurnRightInstruction, MacroInstruction
类都在扮演该角色。
3.1.3 Receiver ( 接收者 或 命令实现者 )
该角色负责 执行具体的命令。本案例中,Robot
类扮演了该角色。
3.1.4 Invoker ( 调用者 )
该角色负责 承接 Client 角色发布的具体命令,调用具体命令的 execute()
,此外,本角色还能 增强命令的功能(如 重复执行、撤销)。本案例中,Controller
类扮演了该角色。
3.1.5 Client ( 命令发布者 )
该角色负责 生成 Receiver 角色的对象实例,根据业务逻辑创建具体命令(同时将 Receiver 的实例绑定到 具体命令 中),给 Invoker 角色发布具体命令。本案例中,Client
类扮演了该角色。
3.2 类图
说明:Invoker 还会聚合部分 ConcreteCommand,用来实现特定的功能,如 重复执行多条指令、撤销某条指令的执行(这个功能需要 Command 定义撤销的接口)等。
4 注意事项
- 谨慎设计命令接口:命令接口的设计应尽可能 简洁 且 通用,以便能够支持多种不同类型的命令。同时,接口中的方法(如
execute()
、undo()
等)应明确其职责和预期行为。 - 合理管理命令对象:
- 创建与销毁:命令对象的创建和销毁应得到妥善管理,以避免 内存泄漏 等问题。在不再需要命令对象时,应及时将其从系统中移除。
- 存储与检索:如果需要支持命令的 撤销(撤销操作应能够恢复到命令执行前的状态)和 重做(重做操作则应能够重新执行已撤销的命令)功能,应设计合理的机制来 存储 和 检索 命令对象。例如,可以使用 栈 等数据结构来保存命令的历史记录。
- 考虑命令的并发执行:如果命令对象可能会 被多个线程同时访问或修改,则需要考虑 线程安全问题。可以通过在命令对象中添加同步机制(如
synchronized
关键字)来确保线程安全。 - 遵循单一职责原则:确保每个命令对象都专注于执行一个特定的操作,避免将多个操作封装在同一个命令对象中。
- 考虑系统复杂度:在决定使用命令模式之前,应评估系统的复杂度和需求。如果系统相对简单且不需要频繁地添加新命令或支持撤销和重做等功能,则可能不需要使用命令模式。
- 性能影响:命令模式可能会引入额外的性能开销(如对象创建和销毁、状态保存和恢复等)。在性能敏感的应用中,需要仔细评估这些开销对系统性能的影响。
5 在源码中的使用
Java 的 Runnable
接口是命令模式的一个经典应用。当创建一个实现了 Runnable
接口的类的实例时,也就创建了一个命令对象。这个对象可以在一个单独的线程中执行(通过 Thread
类的实例来执行)。例如以下代码:
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("This is a thread");
}
}); // 这是一个命令对象
t1.start();
}
}
在代码中,各类扮演的角色如下:
- Command 角色:
Runnable
接口,它提供了run()
方法。 - ConcreteCommand 角色:自定义的实现了
Runnable
接口的实现类(匿名内部类),它实现了run()
方法。 - Receiver 角色:自定义的实现了
Runnable
接口的实现类(匿名内部类),它在run()
方法中执行了与命令相关的操作。 - Invoker 角色:
Thread
类,它使用了start()
方法来执行 ConcreteCommand 角色的run()
方法。 - Client 角色:
Test
类,它生成了 Receiver 角色的实例,并将 ConcreteCommand 角色的实例交给 Invoker 角色来调用。
6 优缺点
优点:
- 降低耦合度:命令模式通过引入 命令接口 和 具体命令类,使得 命令发布者 与 命令实现者 之间的耦合度降低。发布者 只需要知道 命令接口,而不需要知道具体的 实现者 是谁,这样就增加了系统的 灵活性 和 可维护性。
- 易于扩展:当需要添加新的命令时,只需要实现 命令接口,创建新的 具体命令类,而不需要修改现有的代码。这种“开闭原则”的遵循使得系统 易于扩展。
- 支持撤销和重做:命令模式可以方便地实现 撤销(Undo)和 重做(Redo)功能,只需要在命令执行前后保存系统状态,或者让命令本身具有 撤销 和 重做 的能力即可。
- 命令队列和宏命令:命令模式可以 将多个命令组合成一个命令序列或宏命令,从而实现 批量操作。同时,命令也可以被加入到队列中,按照一定的顺序执行,这增加了操作的灵活性。
- 提高系统的可重用性:命令对象可以 作为参数传递给其他对象,也可以 被存储和序列化到磁盘上,从而实现命令的 持久化 和 远程调用。
缺点:
- 可能产生过多的类:对于每一个命令,都需要实现一个具体的命令类,这可能会导致系统中产生大量的类文件。虽然这些类可以很好地封装命令的执行逻辑,但在一些简单的系统中可能会显得过于繁琐。
- 增加了系统的复杂性:命令模式 增加了系统的抽象层次和复杂度,对于一些简单的请求-响应场景,使用命令模式可能会引入不必要的复杂性。
- 性能考虑:在一些 对性能要求极高的系统 中,命令模式可能会引入额外的性能开销。因为每次执行命令时都需要创建命令对象,并通过调用者进行转发,这可能会增加系统的响应时间和内存消耗。
7 适用场景
- 自动化操作:在 计算机安装程序、自动化测试、批量处理任务 等场景中,命令模式可以帮助程序自动执行一系列步骤,而用户无需每次操作时手动输入每一步指令。例如,在计算机安装程序中,程序可以根据所需的操作自动执行几步操作,而用户只需选择对应的项目,而不用担心去记录和执行每一步指令。
- 多线程操作:在 需要处理多线程操作的系统 中,命令模式允许多个线程发出操作命令,程序可以在后台自动发出指令并处理其他业务,而不用等待线程完成操作。这有助于提升程序的并发处理能力和响应速度。
- 图形用户界面(GUI)应用:在 GUI 程序中,用户的每个操作(如点击按钮、菜单项等)都可以封装成一个命令对象。调用者(如按钮控件)在用户交互时触发命令对象的执行方法,而接收者(如文档编辑器、窗口管理器等)则执行具体的操作(如保存文件、关闭窗口等)。这有助于实现用户界面的灵活性和可扩展性。
- 撤销与重做功能:当 系统需要支持撤销和重做功能 时,命令模式可以方便地实现这些功能。通过保存命令的历史记录,并在需要时重新执行或撤销命令,系统可以恢复到之前的状态。
- 系统操作命令频繁变动:当 系统的某项操作具备命令语义,且 命令实现不稳定或经常变化 时,命令模式可以通过解耦请求与实现来降低系统的耦合度。使用抽象命令接口使请求方的代码架构稳定,同时封装接收方具体命令的实现细节。这样,即使命令的实现发生变化,也不会影响到请求方的代码。
- 事务处理:在 数据库系统 或 需要处理事务的应用 中,命令模式可以将事务操作(如 提交、回滚 等)封装为命令对象。调用者(如事务管理器)负责执行事务命令,而接收者(如数据库连接)则执行具体的数据库操作。这有助于实现事务的原子性、一致性、隔离性和持久性(ACID 特性)。
8 总结
命令模式 是一种 行为型 设计模式,它将命令封装到对象中,解开了命令的 实现者 和 发布者 之间的耦合,使得系统更加灵活。此外,通过命令模式,还可以添加 批量执行、撤销、重做 等更加强大的功能。但是,本模式最大的问题就是 由于频繁生成和销毁对象降低了系统的性能,这里可以考虑使用 单例模式 或 享元模式 进行优化。