设计之美【2】命令模式

命令模式

1 定义

HeadFirst设计模式中是这样定义命令模式的。

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。

类图如下:

命令模式类图

如类图所展示的,一个命令模式中参与者主要包括5类:

  1. Client:客户
  2. Command:命令接口
  3. ConcreteCommand:命令对象
  4. Invoker:调用者
  5. Receiver:接收者(执行者)

接下来将通过样例和源码实例进行命令模式各参与者的具体举例,查看理想的命令模式是怎样的,实际生产级环境中的命令模式又有怎样的变形和简化。

2 命令模式样例

以HeadFirst设计模式中的简单样例为例,来看一下命令模式是怎样的表现模式。

该demo是模拟了一个遥控器控制家中电器的场景,通过命令对象的创建,将发出指令的遥控器控制按钮和接收并完成命令的对象(例如灯、门、吊扇等)进行了解耦。

2.1 命令接口(Command)

命令模式中,Command是命令接口,命令对象都需要实现该接口,样例中的命令接口就是Command接口。

/**
 * Command 接口是 命令接口
 *
 * 所有的命令对象都会实现这个包含的一个方法的命令接口
 *
 * @author dongyinggang
 * @date 2020-07-15 16:02
 **/
public interface Command {
    /**
     * execute 方法是 命令方法,所有的命令对象均实现该接口
     *
     * @param
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:03
     */
    void execute();
}

2.2 命令对象(ConcreteCommand)

命令模式中,ConcreteCommand是重写了命令接口方法的命令对象,有具体的方法实现,这里写了两个命令对象类,开灯命令对象类LightOnCommand和开启车库门对象类GarageDoorOpenCommand。

两个类都实现了Command接口,两个类中都包含了命令接收者对象,分别是LightOnCommand中的灯Light类对象,和GarageDoorOpenCommand中的车库门GarageDoor类对象,两个类重写的execute()方法中,都调用了命令接收者对象的方法,分别是Light类的On方法和GarageDoor类的Up方法,完成了命令的执行,达成了开灯和开车库门的目的。

2.2.1 开灯命令对象类
/**
 * LightOnCommand 类是 开灯命令类
 *
 * @author dongyinggang
 * @date 2020-07-15 16:04
 **/
public class LightOnCommand implements Command {

    /**
     * 灯,接收本对象命令的对象
     */
    private Light light;

    public LightOnCommand(Light light){
        this.light = light;
    }

    /**
     * execute 方法是 命令方法,所有的命令对象均实现该接口
     *
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:03
     */
    @Override
    public void execute() {
        light.on();
    }
}
2.2.2 开启车库门对象类
/**
 * GarageDoorOpenCommand 类是 开启车库门命令类
 *
 * @author dongyinggang
 * @date 2020-07-15 16:36
 **/
public class GarageDoorOpenCommand implements Command {

    /**
     * 车库门
     */
    private GarageDoor garageDoor;

    /**
     * GarageDoorOpenCommand 方法是 有参构造方法
     *
     * @param garageDoor 车库门对象
     * @return  车库门命令对象
     * @author dongyinggang
     * @date 2020/7/15 16:43
     */
    public GarageDoorOpenCommand(GarageDoor garageDoor){
        this.garageDoor = garageDoor;
    }

    /**
     * execute 方法是 命令方法,所有的命令对象均实现该接口
     *
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:03
     */
    @Override
    public void execute() {
        garageDoor.up();
    }
}

2.3 命令接收者Receiver

命令接收者是接收并执行命令的实体类,通常是现实生活中真实存在的目标对象,在本例中,是灯、车库门等等。

2.3.1 灯类

灯作为一个实体,有开灯和关灯两个方法,在命令模式中,如果想要分别完成开灯和关灯的操作,就需要创建一个开灯命令对象类和一个关灯命令对象类,在2.2.1中已经有开灯命令对象类了,可以参照其内容再创建一个关灯命令对象类。

/**
 * Light 类是 灯类,命令的接收者
 *
 * @author dongyinggang
 * @date 2020-07-15 16:00
 **/
public class Light {

    /**
     * on 方法是 开灯
     *
     * @param
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:07
     */
    public void on() {
        System.out.println("entity is on !");
    }

    /**
     * off 方法是 关灯
     *
     * @param
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:08
     */
    public void off() {
        System.out.println("entity is off !");
    }
}
2.3.2 车库门类

车库门作为一个接受者实体,有一个打开车库门的方法,在2.2.2中开启车库门对象类调用了其up()方法,完成了打开车库门的命令。

/**
 * GarageDoor 类是 车库门类
 *
 * @author dongyinggang
 * @date 2020-07-15 16:40
 **/
public class GarageDoor {

    /**
     * up 方法是 打开车库门
     *
     * @param
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:41
     */
    public void up(){
        System.out.println("garage door is open !");
    }
}

2.4 调用者Invoker

命令模式中,调用者通常会持调用者持有一个命令对象,并在某个时间节点调用命令的execute()方法,使接收者接受并执行命令。

demo中的调用者是SimpleRemoteControl简易遥控器类,可以通过setCommand(Command command)设置调用者所拥有的命令对象,然后调用buttonWasPressed(),完成按钮点击动作,调用命令对象的execute()方法,进而使接收者执行相应动作。

/**
 * SimpleRemoteControl 类是 简易遥控器类
 * 命令模式中的调用者invoker
 *
 * @author dongyinggang
 * @date 2020-07-15 16:12
 **/
public class SimpleRemoteControl {

    /**
     * 命令对象
     */
    private Command slot;

    public SimpleRemoteControl() {

    }

    /**
     * setCommand 方法是 设置命令对象
     *
     * @param command 命令对象
     * @return void
     * @author dongyinggang
     * @date 2020/7/15 16:15
     */
    public void setCommand(Command command) {
        this.slot = command;
    }

    public void buttonWasPressed() {
        slot.execute();
    }
}

2.5 客户类client

命令模式中,client负责创建一个命令对象,并设置其接收者。

/**
 * RemoteControlTest 类是 测试简易遥控器的使用
 * 本类为命令模式的客户 client
 *
 * @author dongyinggang
 * @date 2020-07-15 16:16
 **/
public class RemoteControlTest {
    public static void main(String[] args) {
        //遥控器是调用者invoker,后续会设置其命令对象
        SimpleRemoteControl remote = new SimpleRemoteControl();
        //创建一个灯对象,是命令的接收和执行者receiver
        Light light = new Light();
        //创建一个开灯命令对象command,将接收者传入
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        //把命令传给调用者
        remote.setCommand(lightOnCommand);
        //执行开灯命令对象的execute方法,执行开灯命令
        remote.buttonWasPressed();
        
        //创建一个车库门对象
        GarageDoor garageDoor = new GarageDoor();
        //创建一个车库门打开命令对象
        GarageDoorOpenCommand garageDoorOpenCommand = new GarageDoorOpenCommand(garageDoor);
        //设置invoker的命令对象为garageDoorOpenCommand
        remote.setCommand(garageDoorOpenCommand);
        //执行开启车库门命令的execute()方法,执行开启车库门命令
        remote.buttonWasPressed();
    }
}

2.6 总结

由上述demo可以知道,在命令模式中,调用链如下:

  1. 客户中实例化调用者,调用者中绑定了命令对象。

  2. 调用者执行自己的方法,方法体内实际调用了命令对象的execute()方法

  3. 命令对象绑定了接收者,接收到调用者的调用指令后,执行execute()方法,该方法内会调用接收者的相关方法。

  4. 被命令对象绑定的接收者接收到来自命令对象的调用指令,执行自己的动作方法完成该动作。

在这个过程中。发出请求的相关代码在调用者内部,执行请求的特定对象在各实体类内部,通过一层命令对象的介入,成功解耦了这两部分代码,在后续新增动作过程中,只需要新增实体类和命令对象,而其请求的相关代码不再需要修改。

但是解耦的代价往往是代码量的增加,反映在命令模式中就是命令对象的加入,在实际生产代码层面,进行场景分析,权衡解耦和代码量增加的效益,才能决定是否使用设计模式完成代码开发。

3 Spring源码中的命令模式

Spring的JdbcTemplate类的源码中存在命令模式的应用,以下为对源码中命令模式各角色的分析。

3.1 命令接口

命令模式中,Command是命令接口,命令对象都需要实现该接口,在此处,命令接口为StatementCallback接口,源码如下:

public interface StatementCallback<T> {
	 //命令方法
   T doInStatement(Statement stmt) throws SQLException, DataAccessException;

}

接口中有一个doInStatement方法,即各命令对象需要重写的方法。

可以看到,JdbcTemplate类中有四个类都实现了本接口,并重写了doInStatement()方法。

四个命令对象

3.2 命令对象

在此处,命令对象有4个。有趣的是,在本例中的命令对象并非独立声明的,而是嵌入到其调用方法中,作为一个内部类的形式进行声明。

作为命令模式中的一环,命令对象的复用可能性较低,因此将其声明为内部类的方式是有可取之处的,这也就和平时我们常见的命令模式样例有了区别,在这个源码样例中,后面还会出现更多和样例不同的代码,

3.2.1 ExecuteStatementCallback

第一个命令对象是JdbcTemplate类中execute(final String sql)方法中的内部类ExecuteStatementCallback,源码如下:

@Override
public void execute(final String sql) throws DataAccessException {
   if (logger.isDebugEnabled()) {
      logger.debug("Executing SQL statement [" + sql + "]");
   }
   //实现命令接口
   class ExecuteStatementCallback implements StatementCallback<Object>, SqlProvider {
      @Override
      public Object doInStatement(Statement stmt) throws SQLException {
         stmt.execute(sql);
         return null;
      }
      @Override
      public String getSql() {
         return sql;
      }
   }
   execute(new ExecuteStatementCallback());
}
3.2.2 QueryStatementCallback

第一个命令对象是JdbcTemplate类中query方法中的内部类QueryStatementCallback,源码如下:

@Override
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 + "]");
   }
   //实现命令接口
   class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
      
      @Override
      public T doInStatement(Statement stmt) throws SQLException {
         ResultSet rs = null;
         try {
            rs = stmt.executeQuery(sql);
            ResultSet rsToUse = rs;
            if (nativeJdbcExtractor != null) {
               rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
            }
            return rse.extractData(rsToUse);
         }
         finally {
            JdbcUtils.closeResultSet(rs);
         }
      }
      @Override
      public String getSql() {
         return sql;
      }
   }
   return execute(new QueryStatementCallback());
}
3.2.3 UpdateStatementCallback

第三个命令对象为 JdbcTemplate类中update方法中的内部类UpdateStatementCallback,源码如下:

@Override
public int update(final String sql) throws DataAccessException {
   Assert.notNull(sql, "SQL must not be null");
   if (logger.isDebugEnabled()) {
      logger.debug("Executing SQL update [" + sql + "]");
   }
   class UpdateStatementCallback implements StatementCallback<Integer>, SqlProvider {
      @Override
      public Integer doInStatement(Statement stmt) throws SQLException {
         int rows = stmt.executeUpdate(sql);
         if (logger.isDebugEnabled()) {
            logger.debug("SQL update affected " + rows + " rows");
         }
         return rows;
      }
      @Override
      public String getSql() {
         return sql;
      }
   }
   return execute(new UpdateStatementCallback());
}
3.2.4 BatchUpdateStatementCallback

第四个命令对象是JdbcTemplate类中batchUpdate方法中的内部类BatchUpdateStatementCallback,源码如下:

@Override
	public int[] batchUpdate(final String... sql) throws DataAccessException {
		Assert.notEmpty(sql, "SQL array must not be empty");
		if (logger.isDebugEnabled()) {
			logger.debug("Executing SQL batch update of " + sql.length + " statements");
		}

		class BatchUpdateStatementCallback implements StatementCallback<int[]>, SqlProvider {

			private String currSql;

			@Override
			public int[] doInStatement(Statement stmt) throws SQLException, DataAccessException {
				int[] rowsAffected = new int[sql.length];
				if (JdbcUtils.supportsBatchUpdates(stmt.getConnection())) {
					for (String sqlStmt : sql) {
						this.currSql = appendSql(this.currSql, sqlStmt);
						stmt.addBatch(sqlStmt);
					}
					try {
						rowsAffected = stmt.executeBatch();
					}
					catch (BatchUpdateException ex) {
						String batchExceptionSql = null;
						for (int i = 0; i < ex.getUpdateCounts().length; i++) {
							if (ex.getUpdateCounts()[i] == Statement.EXECUTE_FAILED) {
								batchExceptionSql = appendSql(batchExceptionSql, sql[i]);
							}
						}
						if (StringUtils.hasLength(batchExceptionSql)) {
							this.currSql = batchExceptionSql;
						}
						throw ex;
					}
				}
				else {
					for (int i = 0; i < sql.length; i++) {
						this.currSql = sql[i];
						if (!stmt.execute(sql[i])) {
							rowsAffected[i] = stmt.getUpdateCount();
						}
						else {
							throw new InvalidDataAccessApiUsageException("Invalid batch SQL statement: " + sql[i]);
						}
					}
				}
				return rowsAffected;
			}

			private String appendSql(String sql, String statement) {
				return (StringUtils.isEmpty(sql) ? statement : sql + "; " + statement);
			}

			@Override
			public String getSql() {
				return this.currSql;
			}
		}

		return execute(new BatchUpdateStatementCallback());
	}

3.3 接收者

本命令模式中的接收者是Statement接口的实现类的对象,在execute()方法中进行声明,

@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
   Assert.notNull(action, "Callback object must not be null");

   Connection con = DataSourceUtils.getConnection(getDataSource());
   Statement stmt = null;
   try {
      Connection conToUse = con;
      if (this.nativeJdbcExtractor != null &&
            this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
         conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }
      stmt = conToUse.createStatement();
      applyStatementSettings(stmt);
      Statement stmtToUse = stmt;
      if (this.nativeJdbcExtractor != null) {
         stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
      }
      T result = action.doInStatement(stmtToUse);
      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.
      JdbcUtils.closeStatement(stmt);
      stmt = null;
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
   }
   finally {
      JdbcUtils.closeStatement(stmt);
      DataSourceUtils.releaseConnection(con, getDataSource());
   }
}

在命令对象重写的doInStatement()方法中执行Statement的方法组合和相应的逻辑代码,和典型样例中不一致的是这里并不完全是调用Statement的方法来执行对应逻辑的,而是其方法和逻辑代码的组合,也就是比样例少了一层接收者层面的逻辑封装。在生产过程中,设计模式融入代码的形式本身也并不是一成不变的。

3.4 调用者

在本源码样例中,调用者实际是由JdbcTemplate类担任的,其在execute()方法中调用了命令方法doInStatement()方法,而此execute()非彼execute()方法,本例中命令接口方法实际被命名为doInStatement(),JdbcTemplate类的execute()方法只是一个普通方法,它的参数就是3.1.2中所述的StatementCallback接口的四个实现类(命令对象)。

再贴出一遍execute()方法的代码,如下:

/**
 * StatementCallback入参是四个实现类
 **/
@Override
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
   Assert.notNull(action, "Callback object must not be null");

   Connection con = DataSourceUtils.getConnection(getDataSource());
   Statement stmt = null;
   try {
      Connection conToUse = con;
      if (this.nativeJdbcExtractor != null &&
            this.nativeJdbcExtractor.isNativeConnectionNecessaryForNativeStatements()) {
         conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
      }
      stmt = conToUse.createStatement();
      applyStatementSettings(stmt);
      Statement stmtToUse = stmt;
      if (this.nativeJdbcExtractor != null) {
         stmtToUse = this.nativeJdbcExtractor.getNativeStatement(stmt);
      }
      //调用doInStatement方法
      T result = action.doInStatement(stmtToUse);
      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.
      JdbcUtils.closeStatement(stmt);
      stmt = null;
      DataSourceUtils.releaseConnection(con, getDataSource());
      con = null;
      throw getExceptionTranslator().translate("StatementCallback", getSql(action), ex);
   }
   finally {
      JdbcUtils.closeStatement(stmt);
      DataSourceUtils.releaseConnection(con, getDataSource());
   }
}

3.5 客户类Client

在本例中,JdbcTemplate类还扮演着客户类的角色,是动作的最初发起人,它在自己的四个方法中调用了execute(StatementCallback action)方法,这四个方法分别是 execute(final String sql)、 query(final String sql, final ResultSetExtractor rse)、update(final String sql)、batchUpdate(final String… sql),以query()方法为例,理解一下是如何进行命令调用的。

/**
 * 命令调用角色-JdbcTemplate类
 **/
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 + "]");
        }
				//声明命令对象类
        class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
            QueryStatementCallback() {
            }

            public T doInStatement(Statement stmt) throws SQLException {
                ResultSet rs = null;

                Object var4;
                try {
                    rs = stmt.executeQuery(sql);
                    ResultSet rsToUse = rs;
                    if (JdbcTemplate.this.nativeJdbcExtractor != null) {
                        rsToUse = JdbcTemplate.this.nativeJdbcExtractor.getNativeResultSet(rs);
                    }

                    var4 = rse.extractData(rsToUse);
                } finally {
                    JdbcUtils.closeResultSet(rs);
                }

                return var4;
            }

            public String getSql() {
                return sql;
            }
        }
				//命令调用,向调用者中传入了命令对象QueryStatementCallback
        return this.execute((StatementCallback)(new QueryStatementCallback()));
    }

根据传递的命令对象的不同,execute()中会执行不同的doInStatement()方法。

3.6 总结

以JDBC模板源码为例,不难发现,在实际应用过程中,命令模式的应用和常规的类图展示是有所区别的。

比较明显的例子就是在这部分源码中,JdbcTemplate类扮演了多重角色:

  1. 通过query()等方法成为命令模式的客户类,担任起请求发起者的角色。
  2. 通过execute()方法成为命令模式的调用者类,担任起调用命令对象的命令接口方法的角色。
  3. 通过在类的query()等四个方法中构建四个内部类作为命令对象的方式成为命令模式的命令对象的外部类,担任起声明命令具体内容的方法。

通过以上方式,在合理解耦“动作的请求者(JdbcTemplate类)”和“动作的执行者(Statement)”的情况下,通过内部类等方式将代码进行了缩减,如果需要对请求者请求的动作进行改动,只需要修改内部类中的相关接口代码,而不会影响其他JdbcTemplate类的其他地方。

但值得注意的是,作为JdbcTemplate类的一部分,如果实际执行动作改变,其代码发生了实际变更,和我们传统意义上的解耦概念是存在冲突的,这种在一定意义上弱化了命令模式解耦功能的源码级应用,究竟是否可以被称为解耦成功,可能就见仁见智了,至少在我看来,其耦合性可以低至忽略不计,其中四个命令类作为内部类由于只被使用一次,不存在复用情形,如果非要依照模板拆分出来,多多少少有些徒增代码量的意味了。

4 参考

  1. 命令模式
  2. 《Head First 设计模式》——命令模式
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值