本文中涉及到的完整代码存放于以下 GitHub 仓库中 LearningCode
1. 理论部分
命令模式(Command Pattern):将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。
命令模式也称动作模式(Action Pattern)或事务模式(Transaction Pattern)。
1.1 结构与实现
命令模式包含以下 4 个角色:
- Command(抽象命令类):
- 职责:声明执行请求的接口。
- 实现:通常声明为抽象类或接口。
- ConcreteCommand(具体命令类):
- 职责:关联一个接收者对象。调用接收者对象的相应操作,以实现了 Command 中声明的接口。
- 实现:通常声明为具体类。
- Invoker(调用者):
- 职责:即请求发送者,它通过命令对象来执行请求。
- 实现:通常声明为具体类。
- Receiver(接收者):
- 职责:执行与请求相关的操作。
- 实现:通常声明为具体类。
在使用过程中,Client(客户端)负责创建 ConcreteCommand(具体命令对象),指定其 Receiver(接收者),并将命令注入调用者(Invoker)中。命令模式的 UML 类图如下所示:
1.2 支持撤销和重做
Command 可以声明 undo 方法和 redo 方法以支持撤销和重做操作。为达到这个目的,ConcreteCommand 需要存储额外的状态信息。这个状态包括:
- Receiver 执行操作时的参数。
- 如果处理请求的操作会改变 Receiver 中的某些值,那么这些值也必须存储起来。Receiver 需要提供一些接口,通过调用该接口可将接收者恢复到它先前的状态。
若 Command 仅支持一次撤销操作,只需要存储最近一次被执行的命令。若要支持多级的撤销和重做,就需要有一个已被执行命令的历史列表,该列表的长度决定了撤销和重做的级数。历史列表存储了已被执行的命令序列,向后遍该列表并逆向执行命令是撤销它们的结果,向前遍历并执行命令是重新执行它们。在存储命令时,应当存储命令的副本。这是因为执行原来的请求的命令对象可能将在稍后执行其他请求,如果命令的状态在各次调用之间会发生变化,那就必须进行拷贝以区分相同命令的不同调用。
可以使用备忘录模式来辅助实现。
1.3 扩展:宏命令
宏命令(Macro Command)又称为组合命令(Composite Command),它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合,在该集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的 execute() 方法时将递归调用它所包含的每个成员命令的 execute() 方法。一个宏命令的成员可以是简单命令,也可以继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。其 UML 类图如下所示:
1.4 扩展:命令队列
有时候,当一个请求发送者发送一个请求时有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理,此时可以通过命令队列来实现。
命令队列的实现方法有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。
1.5 扩展:隐藏 Receiver
有时候,为了简化命令模式,可以向 Client 隐藏 Receiver,即由 ConcreteCommand 创建 Receiver 或者由 ConcreteCommand 合并 Receiver 的职责。
1.6 优缺点与适用场景
命令模式具有以下优点:
- 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
- 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足开闭原则的要求。
- 可以比较容易地设计一个命令队列或宏命令(组合命令)。
- 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
命令模式存在以下缺点:使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,所以在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
命令模式适用于以下场景:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
- 系统需要在不同的时间指定请求,将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换而言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,并且无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起形成宏命令。
2. 实现部分
以 Java 代码为例,演示命令模式的实现。
2.1 命令模式的实现
案例介绍:
定义抽象命令类——Command
public interface Command {
void execute();
}
定义具体命令类,以ExitCommand为例
public class ExitCommand implements Command{
private SystemExitClass systemExitClass;
public ExitCommand(SystemExitClass systemExitClass) {
this.systemExitClass = systemExitClass;
}
@Override
public void execute() {
systemExitClass.exit();
}
}
定义与ExitCommand关联的接收者
public class SystemExitClass {
public void exit() {
System.out.println("退出系统!");
}
}
定义调用者
public class FunctionButton {
private Command command;
public FunctionButton(Command command) {
this.command = command;
}
public void click() {
System.out.print("单击功能键:");
command.execute();
}
}
客户端调用
public class Main {
public static void main(String[] args) {
SystemExitClass receiver = new SystemExitClass();
ExitCommand command = new ExitCommand(receiver);
FunctionButton invoker = new FunctionButton(command);
invoker.click();
}
}
其完整的 UML 类图如下所示:
2.2 扩展:隐藏 Receiver
在上述案例中,可以将 ConcreteCommand 合并 Receiver的职责。
修改ExitCommand代码,由ExitCommand创建SystemExitClass对象。
public class ExitCommand implements Command{
private SystemExitClass systemExitClass = new SystemExitClass();
@Override
public void execute() {
systemExitClass.exit();
}
}
修改客户端调用
public class Main {
public static void main(String[] args) {
ExitCommand command = new ExitCommand();
FunctionButton invoker = new FunctionButton(command);
invoker.click();
}
}
2.3 支持撤销
案例介绍:
这里演示加法的实现。
定义抽象命令类
public interface Command {
int execute(int value);
int undo();
}
定义具体命令类
public class AddCommand implements Command {
private Adder adder;
private int value;
public AddCommand(Adder adder) {
this.adder = adder;
}
@Override
public int execute(int value) {
this.value = value;
return adder.add(value);
}
@Override
public int undo() {
return adder.add(-value);
}
}
定义调用者
public class CalculatorForm {
private Command command;
public CalculatorForm(Command command) {
this.command = command;
}
public void compute(int value) {
int i = command.execute(value);
System.out.println("执行运算,运算结果为:" + i);
}
public void undo() {
int i = command.undo();
System.out.println("执行撤销,运算结果为:" + i);
}
}
定义接收者
public class Adder {
private int num = 0;
public int add(int value) {
num += value;
return num;
}
}
客户端调用
public class Main {
public static void main(String[] args) {
Adder receiver = new Adder();
Command command = new AddCommand(receiver);
CalculatorForm invoker = new CalculatorForm(command);
invoker.compute(10);
invoker.compute(5);
invoker.compute(10);
invoker.undo();
}
}
其完整的 UML 类图如下所示:
3. 参考资料
学习视频:
- 设计模式快速入门 —— 图灵星球TuringPlanet —— 命令模式
- Java设计模式详解 —— 黑马程序员 —— 命令模式(P100 ~ P104)
- Java设计模式 —— 尚硅谷 —— 命令模式(P101 ~ P105)
学习读物:
- 《设计模式:可复用面向对象软件的基础》—— Erich Gamma 著 —— 李英军 译 —— 第 5.2 节(P175)
- 《Java 设计模式》 —— 刘伟 著 —— 第 17 章(P236)
- 《设计模式之美》—— 王争 著 —— 第 8.11 节(P334)
- 《设计模式之禅》 —— 第 2 版 —— 秦小波 著 —— 第 15 章(P162)
- 《图解设计模式》—— 结城浩 著 —— 杨文轩 译 —— 第 22 章(P259)
电子文献: