在现实生活中人们通过开关来控制一些电器的打开和关闭,例如电灯或者排气扇。在购买开关时,人们可能并不知道它将来要用来控制什么电器,也就是说开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。可以将开关理解成一个请求的发送,用户通过它来发送一个“开灯”的请求,而电灯是“开灯”的最终接收者和处理者,开关和电灯之间并不存在直接耦合关系,它们通过电线连接在一起,使用不同的电信可以连接不同的请求接收者,只需换一根电线即可,相同的发送者(开关)即可对应不同的接收者(电器)。
定义
将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持撤销的操作。
结构
Command(抽象命令类)
抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
ConcreteCommand(具体命令类)
具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。具体命令类在实现execute()方法时将调用接收者对象的相关操作(Action)。
Invoker(调用者)
调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
Reciver(接收者)
接收者执行与请求相关的操作,具体实现对请求的业务处理。
举例说明
为了用户使用方便,某系统提供了一系列功能键,用户可以自定义功能键的功能,例如功能键可以退出系统,也可以用来显示帮助文档。
1.抽象命令类
// 抽象命令类
public abstract class Command {
// 执行命令
public abstract void execute();
}
2.请求接收者
// 请求接收者 - 退出系统模拟类
public class SystemExitClass {
public void exit() {
System.out.println("退出系统!");
}
}
// 请求接收者 - 显示帮助文档模拟实现类
public class DisplayHelpClass {
public void display() {
System.out.println("显示帮助文档!");
}
}
3.具体命令类
// 具体命令类 - 退出命令类
public class ExitCommand extends Command {
// 维持对请求接收者的引用
private SystemExitClass exitObj;
public ExitCommand() {
exitObj = new SystemExitClass();
}
@Override
public void execute() {
exitObj.exit();
}
}
// 具体命令类 - 帮助命令类
public class HelpCommand extends Command{
// 维持对请求接收者的引用
private DisplayHelpClass disObj;
public HelpCommand() {
disObj = new DisplayHelpClass();
}
@Override
public void execute() {
disObj.display();
}
}
4.请求发送者
// 请求调用者(请求发送者) - 功能键类
public class FunctionButton {
private Command command;
// 为功能键注入命令
public void setCommand(Command command) {
this.command = command;
}
// 发送请求的方法
public void click() {
System.out.println("单击功能键:");
// 执行命令
command.execute();
}
}
5.测试
public class Client {
public static void main(String[] args) {
FunctionButton fb = new FunctionButton();
Command command;
command = new ExitCommand();
fb.setCommand(command);
fb.click();
System.out.println("------------");
command = new HelpCommand();
fb.setCommand(command);
fb.click();
}
}
6.运行截图
实现命令队列
有时候,当一个请求发送者发送一个请求时有不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理,此时可以通过命令队列来实现。
命令队列的实现方式有多种形式,其中最常用、灵活性最好的一种方式是增加一个CommandQueue类,由该类负责存储多个命令对象,而不同的命令对象可以对应不同的请求接收者。
public class CommandQueue {
// 用来存储命令队列
private ArrayList<Command> commands = new ArrayList<Command>();
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
public void excute() {
for(Command command : commands) {
command.execute();
}
}
}
在增加了命令队列类CommandQueue之后,请求发送者类Invoker将针对CommandQueue编程。
public class Invoker {
// 维持一个CommandQueue对象的引用
private CommandQueue commandQueue;
public Invoker(CommandQueue commandQueen) {
this.commandQueue = commandQueen;
}
public void setCommandQueen(CommandQueue commandQueen) {
this.commandQueue = commandQueen;
}
// 调用CommandQueue类的excute()方法
public void call() {
commandQueue.excute();
}
}
命令队列与我们常说的“批处理”有点类似。当一个发送者发送请求后将有一系列接收者对请求做出响应,命令队列可以用于设计批处理应用程序,如果请求接收者的接收次序没有严格的先后次序,还可以使用多线程技术来并发放调用命令对象的execute()方法,从而提高程序的执行效率。
记录请求日志
请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。很多系统都提供了日志文件,例如Windows日志文件、Oracle日志文件等,日志文件可以记录用户对系统的一些操作(例如对数据的更改)。请求日志文件可以实现很多功能,常用功能如下:
1.一旦系统发生故障,日志文件可以为系统提供一种恢复机制,在请求日志文件中可以记录用户对系统的每一步操作,从而让系统能够顺利恢复到某一特定的状态。
2.请求日志也可以用于实现批处理,在一个请求日志中可以存储一系列命令对象,例如一个命令队列。
3.可以将命令队列中的所有命令对象都存储在一个日志文件中,每执行一个命令则从日志文件中删除一个对应的命令对象,防止因为断点或系统重启等原因造成请求丢失,而且可以避免重新发送全部请求时造成某些命令重复执行,只需读取请求日志文件,再继续执行文件中剩余的命令即可。
在实现请求日志时可以将发送请求的命令对象通过序列化写到日志文件中,此时命令类必须实现Serializable接口。
实现撤销操作
在命令模式中可以通过命令类进行修改使得系统支持撤销操作和恢复操作。
举例说明
设计一个简易计算器,该计算器可以实现简单的数学运算,还可以对运算实施撤销操作。
1.请求接收者
// 请求接收者 - 加法类
public class Adder {
private int num = 0;
public int add(int value) {
num += value;
return num;
}
}
2.抽象命令类
// 抽象命令类
public abstract class AbstractCommand {
// 执行方法
public abstract int execute(int value);
// 撤销方法
public abstract int undo();
}
3.具体命令类
// 具体命令类
public class AddCommand extends AbstractCommand{
private Adder adder = new Adder();
private int value;
@Override
public int execute(int value) {
this.value = value;
return adder.add(value);
}
@Override
public int undo() {
return adder.add(-value);
}
}
4.请求发送者
// 请求发送者
public class CalculatorForm {
private AbstractCommand command;
public void setCommand(AbstractCommand command) {
this.command = command;
}
// 调用命令对象的execute()方法执行运算
public void compute(int value) {
int i = command.execute(value);
System.out.println("执行运算,运算结果为:" + i);
}
// 调用命令对象的undo()方法执行撤销
public void undo() {
int i = command.undo();
System.out.println("执行撤销,运算结果为:" + i);
}
}
5.测试
public class Client {
public static void main(String[] args) {
CalculatorForm form = new CalculatorForm();
AbstractCommand command;
command = new AddCommand();
form.setCommand(command);
form.compute(10);
form.compute(5);
form.undo();
}
}
6.测试截图
本例子,只支持一次撤销。如果想多步撤销,需要引入一个命令集合保存每一次操作时命令的状态,从而实现多次撤销操作。除此之外,恢复操作也是如此实现,恢复操作也即二次撤销。
宏命令
它又称为组合命令,它是组合模式和命令模式联用的产物。宏命令是一个具体命令类,它拥有一个集合,在该集合中包含了对其他命令对象的引用。通常宏命令是一个具体命令类,它拥有一个集合,在集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的excute()方法时将递归调用它所包含的每个成员命令的excute()方法,一个宏命令的成员可以是简单命令,也就是继续是宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理。
优点
1.降低系统耦合度。请求者与接收者之间完全解耦,两者之间具有良好的独立性。
2.新的命令可以很容易地加入到系统中,满足开闭原则。
3.可以比较容易地设计一个命令队列或宏命令。
4.为请求的撤销和恢复操作提供了一种设计和实现方案。
缺点
可能会导致某些系统有过多的具体命令类,这将影响命令模式的使用。