引言
当遇到发送者和接受者之间不是直接连接关系,而是间接连接关系,即发送者和接受者之间需要解耦,我们通常需要命令模式。比如电灯和开关,开关设计时并不知道自己是控制电灯的,也可能控制排气扇、电力设备等等,只有开关和电灯通过电线连接时,它俩之间才可以交互,这其中就蕴含了命令模式的思想。
1.概念
命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。
2.模式结构
![](https://i-blog.csdnimg.cn/direct/3bdb608a675d43b286c3d7e2bc631b7f.png)
3.模式分析
Command:抽象命令类,一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。核心代码如下:
java
public abstract class Command{
public abstract void execute();
}
ConcreteCommand:具体命令类,抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。核心代码如下:
java
class ConcreteCommand extends Command {
private Receiver receiver;//维持一个对请求接收者对象的引用
public void execute(){
receiver.action();//调用请求接收者的业务处理方法action()
}
}
Invoker:调用者即请求发送者,通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。核心代码如下:
java
class Invoker {
private Command command;
//构造注入
public Invoker(Command command) {
this.command = command;
}
//设值注入
public void setCommand (Command command){
this.command = command;
}
//业务方法,用于调用命令类的execute()方法
public void call () {
command.execute();
}
}
Receiver:接收者执行与请求相关的操作,具体实现对请求的业务处理。核心代码如下:
java
public class Receiver {
public void action(){
//具体操作
}
}
4.具体实例分析
![](https://i-blog.csdnimg.cn/direct/39e07dcc262442bc926a5102257c0550.png)
Command:抽象命令接口,声明了执行请求的方法execute()。具体代码如下:
java
// 抽象命令
public interface Command{
public void execute();
}
LightOffCommand:具体命令,表示关灯命令,实现抽象命令接口Command,在类中维护了一个电灯(接收者)的引用,并实现了execute()方法,完成关灯命令的调用。具体代码如下:
java
// 具体命令:关灯命令
class LightOffCommand implements Command {
private LightReceiver light;
public LightOffCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
LightOnCommand:具体命令,表示开灯命令,实现抽象命令接口Command,在类中维护了一个电灯(接收者)的引用,并实现了execute()方法,完成开灯命令的调用。具体代码如下:
java
//具体命令:开灯命令
class LightOnCommand implements Command {
private LightReceiver light;
public LightOnCommand(LightReceiver light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
LightReceiver:命令接收者,表示电灯,它是接收开关发出的命令的角色。具体代码如下:
java
// 接收者:电灯类
class LightReceiver {
void turnOn() {
System.out.println("灯亮了");
}
void turnOff() {
System.out.println("灯灭了");
}
}
SwitchInvoker:请求发送者,表示开关,它是发出开关命令的角色。在这个实例中,抽象命令和具体命令就相当于电线,连接开关和电灯的桥梁。具体代码如下:
java
// 请求发送者:开关类
class SwitchInvoker {
private Command command;
public SwitchInvoker(Command command) {
this.command = command;
}
public void setCommand(Command command) {
this.command = command;
}
public void pressButton() {
command.execute();
}
}
Client:客户端,针对抽象命令编程,可以满足"开闭原则",实现在不改动其他代码的情况下增加具体命令。具体代码如下:
java
public class Client {
public static void main(String[] args) {
LightReceiver light = new LightReceiver();
Command lightOnCommand = new LightOnCommand(light);
Command lightOffCommand = new LightOffCommand(light);
SwitchInvoker switchInvoker = new SwitchInvoker(lightOnCommand);
switchInvoker.pressButton();
switchInvoker.setCommand(lightOffCommand);
switchInvoker.pressButton();
}
}
运行代码,结果如下:
![](https://i-blog.csdnimg.cn/direct/56b7ded760de405e8e78ceaa79615a3b.png)
5.模式扩展
5.1.命令队列
有时候我们需要将多个请求排队,当一个请求发送者发送一个请求时,将不止一个请求接收者产生响应,这些请求接收者将逐个执行业务方法,完成对请求的处理。此时,我们可以通过命令队列来实现。
java
class CommandQueue {
//ArrayList存储命令队列
private ArrayList<Command> commands = new ArrayList<Command>();
public void addCommand(Command command){
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
//循环调用每一个命令对象的execute()方法
public void execute() {
for (Command command:
commands){
command.execute();
}
}
}
而在Invoker中,就需要引入CommandQueue的实例,而不是引入Command的实例,针对CommandQueue编程,调用CommandQueue中的execute()方法。
5.2.撤销
在命令模式中,我们可以通过调用一个命令对象的execute()方法来实现对请求的处理,如果需要撤销(Undo)请求,可通过在命令类中增加一个逆向操作来实现。还可以通过保存对象的历史状态来实现撤销(可使用备忘录模式(Memento Pattern)来实现。)
比如:在计算器中,我们+了一个数,就可以-去一个数来撤销。
5.3.请求日志
请求日志就是将请求的历史记录保存下来,通常以日志文件(Log File)的形式永久存储在计算机中。请求日志文件可以实现:系统恢复机制、命令批处理、命令备份等功能。
5.4.宏命令
宏命令(Macro Command)又称为组合命令,组合模式+命令模式=组合命令。宏命令是一个具体命令类,拥有一个集合属性,在集合中包含了对其他命令对象的引用。通常宏命令不直接与请求接收者交互,而是通过它的成员来调用接收者的方法。当调用宏命令的execute()方法时,将递归调用它所包含的每个成员命令的execute()方法,宏命令的成员可以是简单命令或宏命令。执行一个宏命令将触发多个具体命令的执行,从而实现对命令的批处理,其结构如下图所示:
![](https://i-blog.csdnimg.cn/direct/b55f2af35fca4580afac8f39d46d7c67.png)
6.优缺点
主要优点如下:
(1)降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
(2)新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足"开闭原则"的要求。
(3)可以比较容易地设计一个命令队列或宏命令(组合命令)。
(4)为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案
主要缺点如下:
(1)使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
7.适用情况
(1)系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
(2)系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
(3)系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
(4)系统需要将一组操作组合在一起形成宏命令。