1.需求入手
1.1智能家居的需求
1.我们买了一套智能家电,有照明灯,风扇,冰箱,洗衣机,我们只要在手机上安装App就可以控制这些家电的工作。
2.这些只能家电来自不同的厂家,我们不想针对每一种家电都安装一个APP,分别控制,我们希望通过一个App就可以控制全部的智能家电。
3.要实现一个App控制所有智能家电的需求,则每个智能家电厂家都要提供一个统一的接口给App调用,这时就可以考虑使用命令设计模式。
4.命令模式可将"动作的请求者"从"动作的执行者"对象中解耦出来。
要点:命令模式可以将动作的请求者和动作的执行者进行分离解耦。
5.在这个例子中,请求者时手机APP,动作的执行者是各个厂商的电器,我们通过统一的APP进行执行指令的时候,我们通过统一的手机APP进行下达指令的时候,并不知道哪个电器进行执行相关指令,我们也不需要知道进行指派需要执行指令的具体的电器,只需要下发指令即可,让请求者帮我们完成,完成了请求者和执行者的解耦。
要点:将具体的请求者和具体的执行者进行解耦合。
1.2传统的UML类设计图
可以看到在传统模式下,Client客户端需要进行调用,Client需要知道具体到底要调用的哪个对象,在代码中进行实现的时候,Client需要找到具体要调用的对象,没有实现发布指令的请求者和执行者之间的解耦。

1.3传统模式代码
1.3.1定义电器类
电器类就是具体的执行者。
java
// 空调定义类
public class KongTiao {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void start() {
System.out.println("启动空调");
}
public void end() {
System.out.println("关闭空调");
}
}
1.3.2定义电器遥控
电器遥控就是具体的请求者。
java
// 启动空调
public class KongTiaoController {
private KongTiao kongTiao;
public KongTiao getKongTiao() {
return kongTiao;
}
public void setKongTiao(KongTiao kongTiao) {
this.kongTiao = kongTiao;
}
public void on() {
kongTiao.start();
}
public void off() {
kongTiao.end();
}
}
1.3.3定义调用客户端
使用传统模式的时候,调用客户端,需要找到具体的请求者和具体的接收者,并且具体的请求者和接收者是耦合在一起的,这样就导致整个类的耦合度是较大的。
java
// 客户端
public class Client {
public static void main(String[] args) {
KongTiaoController kongTiaoController = new KongTiaoController();
kongTiaoController.setKongTiao(new KongTiao());
kongTiaoController.on();
kongTiaoController.off();
}
}
2.命令模式
2.1基本介绍
1.命令模式:在软件设计中,我们经常需要某些对象发送请求,但是不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需要在程序运行时指定具体的请求接收者即可
要点:在开始运行的时候,为调用者Invoker进行设置好具体的请求者,进行使用invoker的时候就不需要让invoker直接对被请求者进行调用了,将请求者和执行者之间进行添加了一层缓冲层。
2.命令模式使得请求发送者与请求接收者彼此消除耦合关系,让对象之间的调用关系更加灵活,实现解耦。
要点:如同刚刚的例子,从知道具体的请求者(指定的具体的遥控器),具体的接收者(指定的具体的厂商电器),到现在每个厂商的电器提供统一接口,使用统一的遥控器下发指令,我们只知道下发指令的请求者是谁,但是不知道具体的执行者是谁(请求和执行之间做了一层解耦)
3.在命令模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作。
要点:命令模式中可以进行定义命令的执行(execute)和撤销(undo)
4.通俗易懂的理解:将军发布命令,士兵去执行。其中几个角色:将军(命令发布者),士兵(命令的具体执行者),命令(连接将军和士兵)。
要点:使用封装的命令,将任务的请求者和任务的执行者进行解耦,其实就是在任务的请求者和任务的执行者之间添加了一层。
5.封装的Invoker是调用者(将军),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象。
要点:传统的模式就是调用者(将军)直接去调用被调用者(士兵),整个过程中没有任何缓冲。
2.2命令模式的UML原理类图
和传统模式Invoker(请求者)直接调用Receiver(执行者)那种两层的传统架构不同,使用命令模式会将请求者和执行者之间加一层,也就是将请求者和执行者进行了解耦。

Invoker:就是调用者角色。
Receiver:执行者角色,知道如何执行一个接收到的命令。
Command:抽象命令角色,定义了抽象命令接口,定义了命令的抽象行为execute(执行命令),所有的命令都要进行实现这个接口中定义的抽象行为,同时命令也是通过接口的形式被聚合到Invoker调用者中,进行声明了调用者Invoker可以进行调用的所有命令。
ConcreteCommand:抽象命令实现类,实现了Command命令接口定义的抽象行为execute,负责聚合Receiver执行者,将Receiver聚合到内部,方便调用执行者进行执行具体的指令。
Client:客户端在一开始进行设定好Invoker可以进行调用那些Command命令,也要进行规定好Command命令要聚合那些Receiver执行者,进行执行具体的命令,实施执行。
其实就是新增了一个Command命令层,将Invoker调用者和Receiver执行者进行了解耦合。
2.3命令模式解决智能生活项目的设计
2.3.1需求分析
其实这个需求不是很难,基本上就是将所有家具的功能进行集合在一个遥控器上,定义好按钮(命令)去调用相应的Receiver执行者去执行相应的命令。
Invoker调用者:遥控器
Command命令:遥控器上的按钮
Receiver执行者:各种厂商的电器。
2.3.2UML类图
遥控器:RemoteController => Invoker请求者
抽象命令接口:Command => 抽象命令接口,定义了命令的抽象行为,执行命令execute,撤销命令undo,所以的命令都以命令接口的形式聚合在Invoker调用者中。
具体的命令实例:LightOnCommand,LightOffCommand,NoCommand(设置一个空命令其实也算是一种设计模式,省去判断是否为空的逻辑,提高的代码的可维护性)
具体的命令执行者 :LightReceiver(将自己聚合到需要调用自己的命令中),定义了具体的命令执行逻辑。
客户端:Client,进行指定给Invoker调用者相应的命令对象,以及命令调用的Receiver( 具体接受执行者),使用Invoker去调用命令,命令再去调用相关聚合在命令中的Receiver接收者,完成了Invoker和Receiver之间的解耦。

2.4代码实现
2.4.1执行者的定义
定义执行者进行执行具体的代码。
java
// 实际的执行者
public class LightReceiver {
public void on() {
System.out.println(" 电灯打开了... ");
}
public void off() {
System.out.println(" 电灯关闭了... ");
}
}
2.4.2抽象命令接口的定义
定义抽象命令接口,抽象出命令的方法,执行命令 => execute,撤销命令 => undo。
java
// 创建命令接口
public interface Command {
// 执行动作(操作)
void execute();
// 撤销动作(操作)
void undo();
}
2.4.3命令实例的定义
抽象命令接口实现类进行实现了Command接口,使用构造方法的形式进行聚合具体的执行者Receiver,实现Command接口中定义的抽象方法,调用聚合到命令实现类中的Receiver进行执命令。
定义的开启电灯的命令:
java
// 打开电灯的命令
public class LightOnCommand implements Command {
// 进行设置的具体执行者
private LightReceiver lightReceiver;
// 构造器
public LightOnCommand(LightReceiver lightReceiver) {
this.lightReceiver = lightReceiver;
}
// 执行指令
@Override
public void execute() {
lightReceiver.on();
}
// 撤销这个指令
@Override
public void undo() {
lightReceiver.off();
}
}
定义的关闭电灯的命令:
java
// 电灯关闭的指令
public class LightOffCommand implements Command {
private LightReceiver lightReceiver;
public LightOffCommand(LightReceiver lightReceiver) {
this.lightReceiver = lightReceiver;
}
// 执行这个指令
@Override
public void execute() {
lightReceiver.off();
}
@Override
public void undo() {
lightReceiver.on();
}
}
定义的空命令:
空命令主要是用于,在Invoker调用者中,内聚了一些Command实现类,但是在一开始初始化Invoker的时候不知道要调用哪些命令,如果直接将command对象初始化为null,在进行调用command命令的时候还需要进行一步判空,会非常麻烦,在需要调用command命令的地方都需要判空。
但是如果新建一个空命令类,在Invoker进行创建的时候,内置的command集合中的Command都初始化为空命令,并实现Command,提供抽象方法的空实现,这样就无需判空了,如果调用到还没有初始化的Command,就是调用空命令中的空实现方法,不会执行任何逻辑。
要点:空命令是一种默认空实现的方式,是一种设计模式,被称为空实现设计模式,常用于减少null判空的情景。
java
// 空指令
public class NoCommand implements Command {
@Override
public void execute() {
}
@Override
public void undo() {
}
}
2.4.4具体执行者Receiver的定义
使用数组进行按接口类型聚合命令Command。
定义撤销的命令,这里实现的是单次撤销的命令,如果想要执行多次撤销的命令,可以实现一个List<Command>/数组,记录多次记录,实现多次撤销的操作。
在具体执行者Receiver的构造器中进行初始化聚合的集合中的Command命令对象为空对象。
定义了设置按钮对应Command命令对象的功能。
定义了按下开/管按钮的函数功能。
定义执行撤销按钮的功能。
java
import java.util.Arrays;
// 遥控器 -> 具体的Invoker调用者
public class RemoteController {
// 开启 按钮的命令数组
private Command[] onCommands;
// 关闭 按钮的命令数组
private Command[] offCommands;
// 执行撤销的命令
private Command undoCommand;
// 构造器, 完成对按钮的初始化
public RemoteController() {
onCommands = new Command[5];
offCommands = new Command[5];
Arrays.fill(onCommands, new NoCommand());
Arrays.fill(offCommands, new NoCommand());
}
// 为按钮设置需要的命令
public void setCommand(int no, Command onCommand, Command offCommand) {
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
// 按下开按钮
public void onButtonWasPushed(int no) {
// 找到你按下的开的按钮, 并调用对应的方法
onCommands[no].execute();
// 记录这次操作, 用于撤销
undoCommand = onCommands[no];
}
// 按下关闭按钮
public void offButtonPushed(int no) {
offCommands[no].execute();
undoCommand = offCommands[no];
}
// 按下撤销按钮
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
3.命令模式的应用
3.1命令模式在JDK中的应用
暂无。
3.2命令模式在Spring中的应用
3.2.1命令模式的应用
命令模式主要是在Spring-JDBC中的JdbcTemplate中进行了应用,需要在项目中进行引入Spring-jdbc的jar包。
使用Maven进行引入Spring-JDBC:
java
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.0.RELEASE</version>
</dependency>
3.2.2命令模式的应用UML类图

3.2.3源码解读
3.2.3.1StatementCallback接口的定义
StatementCallback接口其实就是抽象命令接口Command,里面进行定义命令的抽象方法等。
java
import java.sql.SQLException;
import java.sql.Statement;
import org.springframework.dao.DataAccessException;
import org.springframework.lang.Nullable;
@FunctionalInterface
public interface StatementCallback<T> {
@Nullable
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
}
3.2.3.2JdbcTemplate中的execute方法的定义
JdbcTemplate其实就相当于是一个实际请求者Invoker,execute其实就相当于请求者去下发命令的方法,与传统的四人组设定的设计模式不同的是,请求者Invoker进行下发命令的时候,没有将实际的Command内聚到Invoker请求者中,而是通过关联的方式,将Command传入到execute请求方法中,Invoker对传进来的Command进行调用。
java
@Override
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw translateException("StatementCallback", sql, ex);
}
finally {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
3.2.3.3JDBC中query方法定义
query是一个进行发布命令的方法,方法中进行定义了一个内部类QueryStatementCallback,实现了StatementCallback抽象命令接口,即实现了抽象命令接口定义的抽象命令方法,所以说query方法中的内部类QueryCallBack方法是一个实际的命令实现类。
但是QueryStatementCallBack中没有进行聚合进来实际的执行者Receiver,而是直接定义在了QueryStatementCallback中,其实这也可以说是一种组合,将逻辑直接定死在Command实现类中,其实就是相当于直接将Receiver定义在Command实现类中。
所以说内部类QueryStatementCallBack既是一个Command实现类,也是一个实际的执行者Receiver。
query就是一个实际找到命令发布请求的方法,最终query执行了execute,并传进去一个QueryStatementCallback的实例化对象。
java
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
Assert.notNull(sql, "SQL must not be null");
Assert.notNull(rse, "ResultSetExtractor must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL query [" + sql + "]");
}
/**
* Callback to execute the query.
*/
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
@Nullable
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
try {
rs = stmt.executeQuery(sql);
return rse.extractData(rs);
}
finally {
JdbcUtils.closeResultSet(rs);
}
}
@Override
public String getSql() {
return sql;
}
}
return execute(new QueryStatementCallback());
}
3.2.3.4源码调用流程
从整个源码的调用流程中就可以体现出命令设计模式的设计思想,就是将请求者和具体的执行者进行解耦,可以发现query方法并没有直接去调用QueryStatementCallback中的doInStatement的方法,而是通过实例化一个Statement实现类对象(QueryStatementCallback),传入到execute中进行调用Receiver中的方法。
通过execute实现了请求者和命令执行者之间的解耦。

4.命令模式的注意事项和细节
4.1将发起请求的对象与执行请求的对象解耦
命令设计模式可以将发起请求的对象(即Invoker)和执行请求的对象(即Receiver)进行解耦合。发起请求的对象是调用者,调用者只要调用命令对象的execute()方法就可以让接收者工作,而不必知道具体的接收者对象是谁,是如何实现的,命令对象会负责让接收者执行请求的动作,也就是说:请求发起者和请求执行者之间的解耦是通过命令对象实现的,命令对象起到纽带桥梁的作用。
要点:Commend对象充当Invoker和Receiver之间的纽带,将两者解耦连接。
4.2容易设计一个命令队列
只要将命令对象放到队列中,就可以多线程执行命令(让多个线程去命令队列中排队取出命令,进行调用执行)
4.3容易实现对请求的撤销和重做
命令模式允许在Invoker中进行设定一个记录上一步/上多步的字段,设定撤销方法,进行执行上一步/上多步的对应的撤销方法。
4.4命令模式的不足
可能导致系统中有过多的具体命令类(但是这是无法避免的,必须要付出的代价,因为命令模式对应的设计思想就是如此,鱼和熊掌不可兼得),增加了系统的复杂度,这也是在使用时要格外注意的。
4.5空命令对应的设计模式
空命令也是一种设计模式,主要是为了干掉各种判空操作。
在上面的实例中,如果没有空命令的设计,就要在Invoker对命令进行调用的时候,增加一堆判空的操作,增加系统的复杂度,对编码也会带来的一定的麻烦。
4.6命令模式的应用场景
界面的一个按钮就是一条命令,模拟CMD(DOS命令),订单的撤销/恢复,触发-反馈机制。
要点:命令行编程的时候经常使用命令设计模式。