设计模式-命令模式

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

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

相关推荐
CHEN5_023 小时前
设计模式——责任链模式
java·设计模式·责任链模式
前端拿破轮4 小时前
平衡二叉树的判断——怎么在O(n)的时间复杂度内实现?
前端·算法·设计模式
牛奶咖啡135 小时前
学习设计模式《十九》——享元模式
学习·设计模式·享元模式·认识享元模式·享元模式的优缺点·何时选用享元模式·享元模式的使用示例
讨厌吃蛋黄酥5 小时前
🌟 弹窗单例模式:防止手抖党毁灭用户体验的终极方案!
前端·javascript·设计模式
前端拿破轮7 小时前
二叉树的最小深度——和最大深度一样的逻辑?
算法·设计模式·面试
未既9 小时前
java设计模式 -【策略模式】
java·设计模式·策略模式
未既13 小时前
java设计模式 -【装饰器模式】
java·设计模式·装饰器模式
Amagi.13 小时前
Java设计模式-适配器模式
java·设计模式·适配器模式
dongdonglele52113 小时前
C++ 23种设计模式-工厂模式
java·c++·设计模式
枣伊吕波1 天前
第十八节:第八部分:java高级:动态代理设计模式介绍、准备工作、代码实现
java·python·设计模式