命令模式(Command Pattern)
1. 定义
官方定义:命令模式:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
核心在于:
将不同请求封装成不同的命令对象,不同的命令对象包含着处理对应请求的接收者对象;
增加一个调用者角色Invoker,将命令对象传递给调用者对象,由调用者对象去通知接收者处理请求;
命令模式是一种对象行为型模式。
2. 组成部分
由上分析,命令模式有三个重要组成部分:
- Command:请求封装成的对象。也就是说将请求方法封装成一个命令对象,通过操作命令对象来操作请求方法。
- Receiver:命令接收对象。Receiver对象的主要作用就是受到命令后执行对应的操作。
- Invoker:命令的使用者就是Invoker对象。Invoker对象负责要求命令对象执行请求,通常会持有很多命令对象。
3. UML类图
Command:用来声明执行操作的接口;
ConcreteCommand: 具体命令类;
Invoker: 调用者;
Receiver: 接收者;
Client: 客户类;
4. 案例讲解
假设有一个餐厅,有一个厨师,他会做两道菜:酸菜鱼和辣子鸡。当客户点餐,餐厅服务员会记录客户要求的辣度、份量等信息。然后,服务员会将订单交给后厨,厨师开始做饭。
在这个例子中,有三个主要人物:客户、服务员、厨师。如果使用命令模式,则可以理解为:
客户为请求的发送者,将用户的不同请求封装成不同的命令对象;
厨师为命令的接受者,他要根据客户的喜好准备餐点;
服务员为命令的使用者。
我们先设计一个命令接口,为声明执行操作的接口,execute()方法的参数为客户的餐品要求(辣度、份量等)。
public interface Command {
Object execute(String[] args);
}
再声明一个接收者的接口,以及它的抽象实现类;
抽象类里action()方法可定义接受者的通用操作,具有特色的功能由子类实现;这里就不对抽象类做实现。
public interface Receiver {
Object action(String[] args);
}
public abstract class AbstractReceiver implements Receiver {
@Override
public Object action(String[] args) {
return null;
}
}
然后设计一个命令的抽象类,用于获取命令的实际接收者,并调用接受者(厨师)的action()方法。
// 抽象命令类
public abstract class AbstractCommand implements Command {
private AbstractReceiver receiver;
public AbstractCommand(AbstractReceiver receiver) {
this.receiver = receiver;
}
@Override
public Object execute(String[] args) {
receiver.action(args);
return null;
}
}
接下来是谁接收命令呢,当然是厨师,因此我们设计一个命令的接收类Chef,重写抽象父类的方法。
public class Chef extends AbstractReceiver {
@Override
public Object action(String[] args) {
System.out.println("\t订单信息:" + Arrays.toString(args));
return null;
}
}
现在厨师有了,我们将客户的请求封装成命令对象,即设计两个菜品命令类;
有多少个菜品就设计多少个ConcreteCommand,这里就只写以下两个。
// 辣子鸡命令类对象
public class ChickenCommand extends AbstractCommand {
public ChickenCommand(Chef chef) {
super(chef);
}
@Override
public Object execute(String[] args) {
System.out.print("客户点了:辣子鸡");
return super.execute(args);
}
}
// 酸菜鱼命令类对象
public class FishCommand extends AbstractCommand {
public FishCommand(Chef chef) {
super(chef);
}
@Override
public Object execute(String[] args) {
System.out.print("客户点了:酸菜鱼");
return super.execute(args);
}
}
到此为止,我们的命令类和接收类都设计好了。
之后就是Invoker,即命令的使用者。因为不同的命令对象ConcreteCommand拥有共同的抽象父类,因此我们很容易的将这些命令放入一个数据结构(如数组)中记录起来。这里就体现出了命令模式的请求参数化,以达到命令的撤销和恢复。
public class CommandInvoker {
List<History> historyCommand = new ArrayList<>();
public void invoke(Command command, String[] args) {
historyCommand.add(new History(command, args)); // 记录历史命令信息
command.execute(args);
}
// 客户不满意,上一份订单重做一次
public void back() {
if (historyCommand.isEmpty()) {
return;
}
int size = historyCommand.size();
int preIndex = size == 1 ? 0 : size - 2;
History preHistory = historyCommand.get(preIndex);
invoke(preHistory.getCommand(), preHistory.getArgs());
}
}
// 记录订单的信息,包括菜品命令类与客户的其他要求
public class History {
private Command command;
private String[] args;
public History(Command command, String[] args) {
this.command = command;
this.args = args;
}
public void setArgs(String[] args) {
this.args = args;
}
public String[] getArgs() {
return args;
}
public void setCommand(Command command) {
this.command = command;
}
public Command getCommand() {
return command;
}
}
剩下就是Client,负责创建ConcreteCommand,并设置接收者Receiver:
public class Client {
public static void main(String[] args) {
CommandInvoker commandInvoker = new CommandInvoker();
Chef chef = new Chef();
commandInvoker.invoke(new ChickenCommand(chef), new String[]{"小红", "小份", "微辣"});
commandInvoker.invoke(new ChickenCommand(chef), new String[]{"小蓝", "大份", "中辣"});
commandInvoker.invoke(new FishCommand(chef), new String[]{"小黄", "中份", "不辣"});
commandInvoker.back(); // 上份订单重做一次
}
}
运行结果:
通过命令模式完成上述案例,实现了客户和厨师之间彻底解耦。
并且程序扩展性良好,便于之后增加厨师或者菜品。
5. 优点
- 命令模式可以让发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
- 易于扩展,新的命令可以很容易地加入到系统中。
- 可以容易地设计一个命令队列和宏命令(宏命令,就是包含多个命令的命令,即一个组合命令)。
- 可以方便地实现对请求的撤销(Undo)和恢复(Redo)。
6. 适用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的Undo操作和恢复Redo操作。
- 系统需要将一组操作组合在一起,即支持宏命令。
7. 总结
命令模式包含四个重要角色:
- Command中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作;
- 具体命令类:实现了在Command中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中;
- 调用者:即请求的发送者,它通过命令对象来执行请求;
- 接收者:执行与请求相关的操作,它具体实现对请求的业务处理。
命令模式的缺点在于:由于每一个命令都需要设计一个具体命令类,类数量随命令数量增长而增长。可能造成类数量过多。