命令模式(Command Pattern):在软件设计中,我们经常需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,我们只需在程序运行时指定具体的请求接收者即可,此时,可以使用命令模式来进行设计
命名模式使得请求发送者与请求接收者消除彼此之间的耦合,让对象之间的调用关系更加灵活,实现解耦。在命名模式中,会将一个请求封装为一个对象,以便使用不同参数来表示不同的请求(即命名),同时命令模式也支持可撤销的操作
模式结构从上图可以看出命令模式包含如下几个角色:
Invoker 是调用者角色 Command: 是命令角色,需要执行的所有命令都在这里,可以是接口或抽象类 Receiver: 接受者角色,知道如何实施和执行一个请求相关的操作 ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现execute
命令模式的本质就在于将命令进行封装,将发出命令的责任和执行命令的责任分开,是的发送者只需要知道如何发送命令即可,不需要命令是如何实现的,甚至命令执行是否成功都不需要理会。同时命令模式使得请求也变成了一个对象,它像其他对象一样可以被存储和传递。
在面向对象的程序设计中,一个对象调用另一个对象,一般情况下的调用过程是:创建目标对象实例,设置调用参数,调用目标对象的方法。
但是在有些情况下有必要使用一个专门的类对这种调用过程加以封装,我们把这种专门的类称作command类。整个调用过程比较复杂,或者存在多处这种调用,这时候使用command类对调用加以封装,便于功能的再利用,
调用前后需要对调用参数进行某些处理
调用前后需要进行某些额外的操作,比如日志缓存记录历史操作等
智能家电的例子:
我们买了一套智能家电,有照明灯、风扇、冰箱、洗衣机,我们只要在手机上安装app 就可以控制对这些家电工作。 这些智能家电来自不同的厂家,我们不想针对每一种家电都安装一个App,分别控制,我们希望只要一个app就可以控制全部智能家电。 要实现一个app 控制所有智能家电的需要,则每个智能家电厂家都要提供一个统一的接口给app 调用,这时就可以考虑使用命令模式。 命令模式可将“动作的请求者”从“动作的执行者”对象中解耦出来. 在我们的例子中,动作的请求者是手机app,动作的执行者是每个厂商的一个家电产品
思路分析和图解
代码实现
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-19:59
* @description:
*/
public interface Command {
//执行动作(操作)
public void execute();
//撤销动作(操作)
public void undo();
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:00
* @description:
*/
public class LightReceiver {
public void on() {
System.out.println(" 电灯打开了.. ");
}
public void off() {
System.out.println(" 电灯关闭了.. ");
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-19:59
* @description:
*/
public class LightOffCommand implements Command {
// 聚合LightReceiver
LightReceiver light;
// 构造器
public LightOffCommand(LightReceiver light) {
super();
this.light = light;
}
@Override
public void execute() {
// 调用接收者的方法
light.off();
}
@Override
public void undo() {
// 调用接收者的方法
light.on();
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:00
* @description:
*/
public class LightOnCommand implements Command {
//聚合LightReceiver
LightReceiver light;
//构造器
public LightOnCommand(LightReceiver light) {
super();
this.light = light;
}
@Override
public void execute() {
//调用接收者的方法
light.on();
}
@Override
public void undo() {
//调用接收者的方法
light.off();
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:02
* @description:
*/
public class TVReceiver {
public void on() {
System.out.println(" 电视机打开了.. ");
}
public void off() {
System.out.println(" 电视机关闭了.. ");
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:02
* @description:
*/
public class TVOnCommand implements Command {
// 聚合TVReceiver
TVReceiver tv;
// 构造器
public TVOnCommand(TVReceiver tv) {
super();
this.tv = tv;
}
@Override
public void execute() {
// 调用接收者的方法
tv.on();
}
@Override
public void undo() {
// 调用接收者的方法
tv.off();
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:02
* @description:
*/
public class TVOffCommand implements Command {
// 聚合TVReceiver
TVReceiver tv;
// 构造器
public TVOffCommand(TVReceiver tv) {
super();
this.tv = tv;
}
@Override
public void execute() {
// 调用接收者的方法
tv.off();
}
@Override
public void undo() {
// 调用接收者的方法
tv.on();
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:01
* @description:
*/
public class NoCommand implements Command {
@Override
public void execute() {
}
@Override
public void undo() {
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-20:01
* @description:
*/
public class RemoteController {
// 开按钮的命令数组
Command[] onCommands;
Command[] offCommands;
// 执行撤销的命令
Command undoCommand;
// 构造器,完成对按钮初始化
public RemoteController() {
onCommands = new Command[5];
offCommands = new Command[5];
for (int i = 0; i < 5; i++) {
onCommands[i] = new NoCommand();
offCommands[i] = new NoCommand();
}
}
// 给我们的按钮设置你需要的命令
public void setCommand(int no, Command onCommand, Command offCommand) {
onCommands[no] = onCommand;
offCommands[no] = offCommand;
}
// 按下开按钮
public void onButtonWasPushed(int no) { // no 0
// 找到你按下的开的按钮, 并调用对应方法
onCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = onCommands[no];
}
// 按下开按钮
public void offButtonWasPushed(int no) { // no 0
// 找到你按下的关的按钮, 并调用对应方法
offCommands[no].execute();
// 记录这次的操作,用于撤销
undoCommand = offCommands[no];
}
// 按下撤销按钮
public void undoButtonWasPushed() {
undoCommand.undo();
}
}
package com.dongguo.command;
/**
* @author Dongguo
* @date 2021/8/22 0022-19:59
* @description:
*/
public class Client {
public static void main(String[] args) {
//使用命令设计模式,完成通过遥控器,对电灯的操作
//创建电灯的对象(接受者)
LightReceiver lightReceiver = new LightReceiver();
//创建电灯相关的开关命令
LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
//需要一个遥控器
RemoteController remoteController = new RemoteController();
//给我们的遥控器设置命令, 比如no = 0 是电灯的开和关的操作
remoteController.setCommand(0, lightOnCommand, lightOffCommand);
System.out.println("--------按下灯的开按钮-----------");
remoteController.onButtonWasPushed(0);
System.out.println("--------按下灯的关按钮-----------");
remoteController.offButtonWasPushed(0);
System.out.println("--------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
System.out.println("=========使用遥控器操作电视机==========");
TVReceiver tvReceiver = new TVReceiver();
TVOffCommand tvOffCommand = new TVOffCommand(tvReceiver);
TVOnCommand tvOnCommand = new TVOnCommand(tvReceiver);
//给我们的遥控器设置命令, 比如no = 1 是电视机的开和关的操作
remoteController.setCommand(1, tvOnCommand, tvOffCommand);
System.out.println("--------按下电视机的开按钮-----------");
remoteController.onButtonWasPushed(1);
System.out.println("--------按下电视机的关按钮-----------");
remoteController.offButtonWasPushed(1);
System.out.println("--------按下撤销按钮-----------");
remoteController.undoButtonWasPushed();
}
}
运行结果:
--------按下灯的开按钮-----------
电灯打开了..
--------按下灯的关按钮-----------
电灯关闭了..
--------按下撤销按钮-----------
电灯打开了..
=========使用遥控器操作电视机==========
--------按下电视机的开按钮-----------
电视机打开了..
--------按下电视机的关按钮-----------
电视机关闭了..
--------按下撤销按钮-----------
电视机打开了..
优点
1. 降低了系统耦合度
2. 新的命令可以很容易添加到系统中去。
缺点使用命令模式可能会导致某些系统有过多的具体命令类。
模式使用场景1.系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
2.系统需要在不同的时间指定请求、将请求排队和执行请求。
3.系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
5.系统需要将一组操作组合在一起,即支持宏命令。
命令模式在Spring 框架JdbcTemplate 应用的源码分析Spring 框架的JdbcTemplate 就使用到了命令模式
首先先注入jdbctemplate 调用 queryForObject 方法
其实每个方法底层实现都一样,就用这个举例吧。点进去这个方法,一路跟进去,找到最深那个query方法在中间一直都在构建查询需要的参数,可以跳过,最深的query方法如下
@Override
@Nullable
public T query(final String sql, final ResultSetExtractor 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 + "]");
}
class QueryStatementCallback implements StatementCallback, 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());
}
这里面有个内部类 QueryStatementCallback 实现了 StatementCallback 接口
而这个接口只有一个方法
@Nullable
T doInStatement(Statement stmt) throws SQLException, DataAccessException;
通过类图可以看到这个接口还有好几个具体实现类
上面说的QueryStatementCallback 只是其中一个,然后我们看看这个类具体实现方法 doInStatement
本质上就是调用这个方法的参数的 executeQuery 方法 执行sql。
最后就是创建了这个内部类的实例传给 execute方法 ,点进去,
@Override
@Nullable
public T execute(StatementCallback 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());
}
}
第11行,就是调用真正内部类的实现,在调用之前,需要构建方法所需参数stmt , 然后返回结果
那怎么和命令模式联系起来呢?我们来和命令模式5种角色,对号入座:
Command 其实 就是 StatementCallback ,里面的doInStatement就是 命令方法。
CommandImpl 就是 QueryStatementCallback ,由命令的具体实现去调用真正的执行方法,真正的执行方法就是stmt的executeQuery()。
Receiver 呢?jdbctemplate 是没有具体的,但是我们可以把 Statement 看做是这个receiver ,前面的命令模式说的是,命令的具体实现拥有接收者,并通过构造方法赋值,而template 它是把接收者当作参数传递,我觉得本质是一样的
因为之前实例化命令的具体实现时,传入接收者,当然先的构造一个接收者,而template 也是一样,先构造statement 然后传入。
Invoker 就相当于 public T execute(StatementCallback action) throws DataAccessException 这个方法。
Client 就相当于我们自己开发的应用程序。
可以看出这个invokrer 也是传入不同的具体命令,执行不同的 命令,达到不一样的结果。
总结1. 命令模式的本质就是将命令对象进行封装打包,将发出命令的责任和执行命令的责任进行割开。
2. 命令模式中发送者只需要知道如何发送请求命令,无须关心命令执行具体过程。
3. 在发送者和接收者两者间是通过命令对象进行沟通的。请求命令本身就当做一个对象在两者间进行传递,它封装了接收者和一组动作。
4. 命令模式支持撤销。
5. 命令模式队列请求和日志请求。