设计模式-命令模式

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命令),订单的撤销/恢复,触发-反馈机制。

要点:命令行编程的时候经常使用命令设计模式。

相关推荐
杨充12 小时前
10.可测试性实战设计
设计模式·开源·代码规范
杨充12 小时前
9.重构十二式的实战
设计模式·开源·代码规范
杨充12 小时前
6.设计原则的全景图
设计模式·开源·全栈
杨充12 小时前
2.面向对象的特性
设计模式
杨充12 小时前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充13 小时前
8.反模式与坏味道
设计模式·开源·代码规范
杨充13 小时前
3.接口vs抽象类比较
设计模式
咖啡八杯16 小时前
文法、BNF与AST
java·设计模式·解释器模式·ast·文法
咖啡八杯1 天前
GoF设计模式——解释器模式
java·后端·spring·设计模式