命令模式
命令模式
案例
一天张三发了奖金,决定今天不做饭,到外面去吃,体验一下被服务的感觉。于是就到楼下李四的餐馆去吃饭,他看着李四拿来的菜单,上面写着:
炒菜 | 凉菜 |
---|---|
回锅肉 | 凉拌三丝 |
土豆丝 | 酿黄瓜 |
。。。 | 。。。 |
然后他就让李四做了回锅肉和凉拌三丝两个菜,李四就按照他点的才去做了。李四在做菜的时候,张三就想着把这一过程用代码的方式表达出来:
1.首先创建厨师类:
/**
* 厨师类
*/
public class Chef {
private String name;
public Chef(String name) {
this.name = name;
}
// 做炒菜
public void cookStirFry(String dishName){
System.out.println("厨师:" + this.name + "\t做炒菜:" + dishName);
}
// 做凉菜
public void cookColdDish(String dishName){
System.out.println("厨师:" + this.name + "\t做凉菜:" + dishName);
}
}j
2.再创建客户类:
/**
* 顾客类
*/
public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
// 点炒菜
public void orderStirFry(String dishName, Chef chef){
System.out.println(this.name + "点了炒菜:" + dishName);
chef.cookStirFry(dishName);
}
// 点凉菜
public void orderColdDish(String dishName, Chef chef){
System.out.println(this.name + "点了凉菜:" + dishName);
chef.cookColdDish(dishName);
}
}
3.模拟过程类:
public class Main {
public static void main(String[] args) {
Customer customer = new Customer("张三");
Chef chef = new Chef("李四");
customer.orderStirFry("回锅肉", chef);
customer.orderColdDish("凉拌三丝", chef);
}
}
4.模拟结果:
张三点了炒菜:回锅肉
厨师:李四 做炒菜:回锅肉
张三点了凉菜:凉拌三丝
厨师:李四 做凉菜:凉拌三丝
张三吃了李四做的菜之后,赞不绝口,加上今天发了奖金,心里很是高兴。他看着李四忙碌的身影,觉得李四也是不容易,一个人又当厨师又当服务员太累了,不仅要对顾客服务,还要去炒菜。如果请一个服务员来帮他点菜,他就只用炒菜,这样就轻松多了,就像是设计模式中的命令模式一样。
模式介绍
在软件系统中,“行为请求者”与“行为实现者”通常呈现一种“紧耦合”。但在某些场合,比如要对行为进行“记录、撤销/重做、事务”等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。这就是命令模式(Command Pattern)。
角色构成
- Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
- ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
- Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
- Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
UML类图
可以看到通过引入了命令类(Command),使得请求者(Invoker)与接收者(Receiver)之间进行了解耦,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
代码改造
通过命令模式的介绍,下面就对案例代码进行改造。
1.首先是请求接收者--厨师类:
/**
* 厨师类:接收者角色
*/
public class Chef {
private String name;
public Chef(String name) {
this.name = name;
}
public void cookStirFry(String dishName){
System.out.println("厨师:" + this.name + "\t做炒菜:" + dishName);
}
public void cookColdDish(String dishName){
System.out.println("厨师:" + this.name + "\t做凉菜:" + dishName);
}
}
2.抽象出菜肴类:
/**
* 菜肴类:抽象命令类角色
*/
public interface Dish {
void order(String dishName);
}
2.两个菜肴实现类:
凉拌菜类:
/**
* 凉拌菜类:具体命令类角色
*/
public class ColdDish implements Dish {
private Chef chef;
public ColdDish(Chef chef) {
this.chef = chef;
}
@Override
public void order(String dishName) {
// 调用实际接收者的动作
chef.cookColdDish(dishName);
}
}
炒菜类:
/**
* 炒菜类:具体命令类角色
*/
public class StirFry implements Dish {
private Chef chef;
public StirFry(Chef chef) {
this.chef = chef;
}
@Override
public void order(String dishName) {
// 调用实际接收者的动作
chef.cookStirFry(dishName);
}
}
3.引入请求者角色--服务员类:
/**
* 服务员类:请求者角色
*/
public class Waiter {
private String name;
private Dish dish;
public Waiter(String name) {
this.name = name;
}
public void setDish(Dish dish) {
this.dish = dish;
}
public void orderDish(String dishName) {
this.dish.order(dishName);
}
}
4.客户实用类:
/**
* 客户实用类
*/
public class Customer {
private String name;
public Customer(String name) {
this.name = name;
}
public void orderStirFry(String dishName, Chef chef) {
System.out.println(this.name + "点了炒菜:" + dishName);
chef.cookStirFry(dishName);
}
public void orderColdDishes(String dishName, Chef chef) {
System.out.println(this.name + "点了凉菜:" + dishName);
chef.cookColdDish(dishName);
}
}
5.测试类:
public class Main {
public static void main(String[] args) {
Chef chef = new Chef("李四");
ColdDish coldDish = new ColdDish(chef);
StirFry stirFry = new StirFry(chef);
Customer customer = new Customer("张三");
Waiter waiter = new Waiter("王五");
waiter.setDish(stirFry);
customer.orderStirFry("回锅肉", waiter);
waiter.setDish(coldDish);
customer.orderColdDishes("凉拌三丝", waiter);
}
}
6.测试结果:
张三点了炒菜:回锅肉
厨师:李四 做炒菜:回锅肉
张三点了凉菜:凉拌三丝
厨师:李四 做凉菜:凉拌三丝
可以看到我们通过引入抽象命令类Dish
,请求发送者Waiter
针对抽象命令类Dish
编程,只有实现了抽象命令类的具体命令才与请求接收者Chef
相关联。这样Chef
李四只用知道他要做什么菜,负责做菜就可以了,点菜的事情交给Waiter
王五来做,使得职责分离开来,更加明确各自的任务。
模式应用
命令这一思想在使用时还是比较常见的,下面就来分析一下其在 Spring JdbcTemplate 中的应用。首先来一段代码:
1.引入依赖:
<properties>
<spring.version>5.1.15.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
2.创建实体类:
public class User {
private String id;
private String username;
@Override
public String toString() {
return "User{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
'}';
}
// 省略getter/setter
}
3.测试类:
public class Main {
public static void main(String[] args) {
// 1.配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
// 2.配置 jdbcTemplate
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
// 3.查询数据
String sql = "select id,username from user";
List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
System.out.println(userList);
}
}
4.测试结果:
[User{id='1', username='张三'}]
这里只是简单的演示了一下JdbcTemplate
类中提供的查询方法,很明显这里要研究的就是jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class))
这个方法:
1.首先调用JdbcTemplate
类中的query(String sql, RowMapper<T> rowMapper)
:
@Override
public <T> List<T> query(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return result(query(sql, new RowMapperResultSetExtractor<>(rowMapper)));
}
2.它在其中调用了同类中的query(final String sql, final ResultSetExtractor<T> rse)
@Override
@Nullable
public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
// ...
/**
* Callback to execute the query.
*/
class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
@Override
@Nullable
// 定义 StatementCallback 接口实现类,实现 doInStatement() 方法
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.这一步在其中创建了QueryStatementCallback
类实例之后,调用了同类的execute(StatementCallback<T> action)
方法:
@Override
@Nullable
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
// ...
Connection con = DataSourceUtils.getConnection(obtainDataSource());
Statement stmt = null;
try {
stmt = con.createStatement();
applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
handleWarnings(stmt);
return result;
}
// ...
}
可以看到在这一步它实际调用的是传入的StatementCallback
实例的doInStatement()
方法,其实这里就符合命令模式的思想。下面是分析思路:
- 首先在第2步调用的
query(final String sql, final ResultSetExtractor<T> rse)
方法中定义的类QueryStatementCallback
可以看作是具体的命令者角色,它实现的接口StatementCallback
就是抽象命令者角色。 - 然后从命令模式的结构来看命令者角色是由请求者使用调用的。所以接着看第3步的方法
execute(StatementCallback<T> action)
,这一方法就可以近似看作是命令模式中的请求者角色,因为在其内部它使用了命令者角色StatementCallback
的方法action.doInStatement(stmt)
,这里根据传入的不同的具体命令类角色类的实例可以起到不用的效果,这里使用的是其中一个实现类QueryStatementCallback
,其他实现类包括:BatchUpdateStatementCallback、ExecuteStatementCallback、UpdateStatementCallback
等实现,它们都是JdbcTemplate
内方法中定义的内部类。 - 而接收者是由命令者角色调用的,所以在第3步中由
QueryStatementCallback
实例调用时T result = action.doInStatement(stmt)
传入的参数Statement
接口的实例则看作是接收者角色。
总结
1.主要优点
- 降低系统的耦合度。由于请求者与接收者之间不存在直接引用,因此请求者与接收者之间实现完全解耦,相同的请求者可以对应不同的接收者,同样,相同的接收者也可以供不同的请求者使用,两者之间具有良好的独立性。
- 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足“开闭原则”的要求。
- 可以比较容易地设计一个命令队列或宏命令(组合命令)。
- 为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
2.主要缺点
- 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
3.适用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无须知道接收者的存在,也无须知道接收者是谁,接收者也无须关心何时被调用。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起形成宏命令。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 关于spring框架JdbcTemplate中的命令模式
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/command
转载请说明出处,本篇博客地址:https://www.cnblogs.com/phoegel/p/14135340.html