名称
命令模式(COMMAND),别名:动作(Action),事务(Transaction)
目的
将一个请求封装为一个对象,从而可以用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
适用性
当有如下需求时,可使用Command模式:
抽象出待执行的动作以参数化某对象。用过程语言中的回调(callback)函数表达这种参数化机制。所谓回调函数是指函数先在某处注册,而它将在稍后某个需要的时候被调用。Command模式是回调机制的一个面向对象的替代品。
在不同的时刻指定、排列和执行请求。一个Command对象可以有一个与初始请求无关的生存期。如果一个请求的接收者可用一种与地址空间无关的方式表达,那么就可将负责该请求的命令对象传送给另一个不同的进程并在那儿实现该请求。
支持取消操作。Command的执行操作可在实施操作前将状态存储起来,在取消操作时这个状态用来消除该操作的影响。Command接口必须添加一个Unexecute操作,该操作取消上一次Execute调用的效果。执行的命令被存储在一个历史列表中。可通过向后和向前遍历这一列表并分别调用Unexecute和Execute来实现重复数量不限的“取消”和“重做”。
支持修改日志,这样当系统崩溃时,这些修改可以被重做一遍。在Command接口中添加装载操作和存储操作,可以用来保持变动的一个一致的修改日志。从崩溃中恢复的过程包括从磁盘中重新读入记录下来的命令并用Execute操作重新执行它们。
用构建在原语操作上的高层操作构造一个系统。这样一种结构在支持事务(transaction)的信息系统中很常见。一个事务封装了对数据的一组变动。Command模式提供了对事务进行建模的方法。Command有一个公共的接口,使得你可以用同一种方式调用所有的事务。同时使用该模式也易于添加新事务以扩展系统。
结构
命令(Command):声明执行操作的接口。
具体命令(ConcreteCommand):
将一个接收者对象绑定于一个动作。
调用接收者相应的操作,以实现Execute。
调用者(Invoker):要求该命令执行这个请求。
接收者(Receiver):知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
协作
Client创建一个ConcreteCommand对象并指定它的Receiver对象。
某Invoker对象存储该ConcreteCommand对象。
该Invoker通过调用Command对象的Execute操作来提交一个请求。若该命令是可撤消的,ConcreteCommand就在执行Excute操作之前存储当前状态以用于取消该命令。
ConcreteCommand对象对调用它的Receiver的一些操作以执行该请求。
下图展示了这些对象之间的交互。它说明了Command是如何将调用者和接收者(以及它执行的请求)解耦的。
效果
优点
Command模式将调用操作的对象与知道如何实现该操作的对象完全解耦。
Command是头等的对象,可以像其他的对象一样被操纵和扩展。
可以将多个命令装配成一个复合命令。一般说来,复合命令是Composite模式的一个实例。
增加新的Command很容易,因为这无需改变已有的类。只需要定义新的命令类而不必修改已有的类或接口。
易于实现事务操作。多个命令可以组合成一个事务,这样就可以作为一个整体来执行或撤销。
支持取消(undo)和重做(redo)。需要Command提供逆转的执行(具体命令),ConcreteCommand类存储额外的状态信息,这些状态信息包括:
接收者对象,它真正执行处理该请求的各操作。
接收者上执行操作的参数。
如果处理请求的操作会改变接收者对象中的某些值,那么这些值也必须先存储起来。接收者还必须提供一些操作,以使该命令可将接收者恢复到它先前的状态。若应用只支持一次取消操作,那么只需存储最近一次被执行的命令。而若要支持多级的取消和重做,就需要有一个已被执行命令的历史表列(historylist),该表列的最大长度决定了取消和重做的级数。历史表列存储了已被执行的命令序列。向前遍历该表列并逆向执行命令是取消(undo)它们的结果;向后遍历并执行命令是重执行(redo)它们。
可以将命令按照特定队列执行。
缺点
可能导致命令类的膨胀。每个命令都需要创建一个具体的命令类,如果命令较多,可能会导致类的数量增加。
可能引入额外的复杂性。引入命令对象和接收者对象之间的关系,可能会增加代码的复杂性。
可能增加内存消耗。如果系统需要支持撤消操作,那么所有的命令对象都需要保存起来,这可能会导致内存消耗增加。
执行效率问题。由于增加了额外的抽象层,可能会对性能造成一定影响,尤其是在频繁执行命令的情况下。
应用
问题
命令模式最大的特点就是将命令的调用者和接收者完全解耦,调用者在调用之前完全不知道接收者是谁。当我们用小爱同学遥控空调的时候,这个时候就可以清楚的知道,调用者是小爱同学,命令是小爱同学发送给空调的信号,具体命令是开空调等信号,接收者是空调,在操作的时候调用者才去确定接收者、具体命令,在操作之前,小爱同学和空调是没有关系的,小爱同学还有可能遥控电视、风扇等,此时就是另一套命令和接收者了,做到了完全解耦。它们之间的调用链是Client -> invoker -> ConcreteCommand(引用是Command,调用时传入)-> receiver(ConcreteCommand内的一个receiver对象,调用时传入)。
根据这一特性,我们使用java代码和命令模式模拟小爱同学控制空调的过程,他们的关系上面已经讲了,接下来我们讲一下如何支持取消。想要支持取消,必须要记录执行的命令以及参数(放在具体命令中),然后在命令中声明cancel()方法并在各个具体命令中取实现它,开空调和关空调的逆转执行命令很简单,分别是关空调和开空调,而改变温度和改变模式要怎么去逆转呢?所谓的逆转不是执行最后一条命令之前的一条命令(倒数第二条命令),而是对最后一条命令造成的影响进行还原,就以改变温度为例,假如最后一步操作之前温度是25度,最后一步操作是改变温度为28度,那逆转操作就是调用改变温度的方法把空调温度改变为25度。之前我们说了,想要实现取消就要记录执行的命令以及参数,我们就可以把改变之前的数值记录在具体命令内以pre开头,取消的时候直接执行changeXXX(preXXX)就可以了,而每个具体命令的参数、改变前的数值与其他具体命令之间存在天然的隔离(如改变温度和改变模式,参数、改变前数值没有交集),所以放在具体命令里面就很合适。
注意:因为取消是对之前的操作进行逆转执行,所以receiver内就不需要cancel方法,具体命令也不需要取消取消命令。实现重做功能也是如此。
示例
UML
代码示例
命令:命令接口,这里的接口是广义上的接口,也可以是抽象类,表示抽象就行(甚至抽象都不需要)
package com.ysj.part5.command.command;
import cn.hutool.json.JSONObject;
/**
* 命令接口
*/
public interface Command {
/**
* 执行命令
*/
void execute();
/**
* 取消命令
*/
void cancel();
}
具体命令:修改模式命令、修改温度命令、打开命令、关闭命令,实现Command
package com.ysj.part5.command.concreteCommand;
import com.ysj.part5.command.command.Command;
import com.ysj.part5.command.receiver.AirCondition;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
/**
* 修改模式命令
*/
public class ChangeModelCommand implements Command {
private AirCondition airCondition;
/**
* 上次的模式:制冷、制热、换风、除湿
*/
private String preModel;
/**
* 当前模式:制冷、制热、换风、除湿
*/
private String currentModel;
public ChangeModelCommand(AirCondition airCondition, String currentModel) {
this.airCondition = airCondition;
this.currentModel = currentModel;
}
@Override
public void execute() {
// 记录改变前的模式,以便取消时使用
preModel = airCondition.getCurrentModel();
// 改变模式
airCondition.changeModel(currentModel);
}
@Override
public void cancel() {
airCondition.changeModel(preModel);
}
}
package com.ysj.part5.command.concreteCommand;
import com.ysj.part5.command.command.Command;
import com.ysj.part5.command.receiver.AirCondition;
/**
* 修改温度命令
*/
public class ChangeTemperatureCommand implements Command {
private AirCondition airCondition;
/**
* 上次的温度
*/
private String preTemperature;
/**
* 当前温度
*/
private String currentTemperature;
public ChangeTemperatureCommand(AirCondition airCondition, String currentTemperature) {
this.airCondition = airCondition;
this.currentTemperature = currentTemperature;
}
@Override
public void execute() {
// 记录改变前的温度,以便取消时使用
preTemperature = airCondition.getCurrentTemperature();
// 改变温度
airCondition.changeTemperature(currentTemperature);
}
@Override
public void cancel() {
airCondition.changeTemperature(preTemperature);
}
}
package com.ysj.part5.command.concreteCommand;
import com.ysj.part5.command.command.Command;
import com.ysj.part5.command.receiver.AirCondition;
/**
* 打开命令
*/
public class OnCommand implements Command {
private AirCondition airCondition;
public OnCommand(AirCondition airCondition) {
this.airCondition = airCondition;
}
@Override
public void execute() {
airCondition.on();
}
@Override
public void cancel() {
// 开空调的逆向执行是关
airCondition.off();
}
}
package com.ysj.part5.command.concreteCommand;
import com.ysj.part5.command.command.Command;
import com.ysj.part5.command.receiver.AirCondition;
/**
* 关闭命令
*/
public class OffCommand implements Command {
private AirCondition airCondition;
public OffCommand(AirCondition airCondition) {
this.airCondition = airCondition;
}
@Override
public void execute() {
airCondition.off();
}
@Override
public void cancel() {
// 关空调的逆向执行是开
airCondition.on();
}
}
接收者:空调,调用链的最终执行端
package com.ysj.part5.command.receiver;
import lombok.Data;
/**
* 空调
*/
@Data
public class AirCondition {
/**
* 当前模式:制冷;制热
*/
private String currentModel = "制冷";
/**
* 当前温度
*/
private String currentTemperature = "25";
/**
* 打开空调
*/
public void on() {
System.out.println("打开空调————当前模式为" + currentModel + ",当前温度为" + currentTemperature + "度");
}
/**
* 关闭空调
*/
public void off() {
System.out.println("关闭空调");
}
/**
* 切换空调模式
*
* @param param
*/
public void changeModel(String param) {
currentModel = param;
System.out.println("切换空调模式为" + param + "————当前模式为" + currentModel + ",当前温度为" + currentTemperature + "度");
}
/**
* 切换空调温度
*
* @param param
*/
public void changeTemperature(String param) {
currentTemperature = param;
System.out.println("切换空调温度为" + param + "度" + "————当前模式为" + currentModel + ",当前温度为" + currentTemperature + "度");
}
}
客户端:人
package com.ysj.part5.command;
import com.ysj.part5.command.concreteCommand.ChangeModelCommand;
import com.ysj.part5.command.concreteCommand.ChangeTemperatureCommand;
import com.ysj.part5.command.concreteCommand.OffCommand;
import com.ysj.part5.command.concreteCommand.OnCommand;
import com.ysj.part5.command.invoker.XiaoaiClassmat;
import com.ysj.part5.command.receiver.AirCondition;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
// 接收者:空调
AirCondition airCondition = new AirCondition();
// 调用者:小爱同学
XiaoaiClassmat xiaoaiClassmat = new XiaoaiClassmat();
// 调用者使用具体命令发送给接收者
System.out.println("请输入命令并按回车,需要帮助请输入help");
// 控制台输入命令
while (true) {
Scanner scanner = new Scanner(System.in);
String command = scanner.nextLine();
switch (command) {
case "on":
xiaoaiClassmat.execute(new OnCommand(airCondition));
break;
case "off":
xiaoaiClassmat.execute(new OffCommand(airCondition));
break;
case "temp":
System.out.println("请输入温度");
xiaoaiClassmat.execute(new ChangeTemperatureCommand(airCondition, scanner.nextLine()));
break;
case "model":
System.out.println("请输入模式(制冷、制热、换风、除湿)");
xiaoaiClassmat.execute(new ChangeModelCommand(airCondition, scanner.nextLine()));
break;
case "cancel":
xiaoaiClassmat.cancel();
break;
case "help":
System.out.println("on:打开空调\noff:关闭空调\ntemp:设置温度\nmodel:设置模式\ncancel:取消上一次命令\nhelp:帮助\nexit:退出");
break;
case "exit":
System.exit(0);
return;
default:
System.out.println("命令错误");
break;
}
}
}
}
在控制台输入各个命令操作空调,结果如下图
已知应用
Spring Framework
Spring 支持基于 AspectJ 的 AOP(面向切面编程),而 AspectJ 在某些场景下会用到命令模式来处理事务管理等操作。
评论区