命令模式(Command):把请求封装成对象
命令模式,说白了就是把「你要干什么」这件事,包装成一个对象。你想想看,在代码里我们经常要调用某个方法,比如 orderService.submit(order),这是直接调用。但有时候,我们希望把「提交订单」这个动作本身作为一个参数传来传去,或者放到队列里排队执行,甚至支持撤销——这时候,命令模式就派上用场了。
我个人习惯把命令模式理解为「任务票」。你拿着票,不一定马上执行,可以攒着、可以排队、甚至可以退票。这种灵活性,在很多场景下非常有用。
命令模式的结构
命令模式的核心角色有四个:
- Command(抽象命令):声明执行操作的接口,通常就一个
execute()方法。 - ConcreteCommand(具体命令):实现 Command 接口,内部持有接收者对象,在
execute()中调用接收者的具体方法。 - Receiver(接收者):真正干活的对象,知道如何实现具体的业务逻辑。
- Invoker(调用者/请求者):持有命令对象,在某个时机调用命令的
execute()方法。
我画了一张图,帮你理清它们之间的关系:
你看这个图,Client 负责创建 ConcreteCommand,并把它设置到 Invoker 里。Invoker 不关心命令内部怎么实现,它只管调用 execute()。ConcreteCommand 内部持有 Receiver,在 execute() 里调用 Receiver 的 action()。这就是典型的「调用者与接收者解耦」。
核心思想:命令模式把「请求」变成了一个独立的对象。你可以对这个对象做任何事——存起来、传参、排队、记录日志、甚至撤销。
命令模式在 Runnable 接口中的应用
说到命令模式在 Java 里的经典应用,我第一个想到的就是 Runnable 接口。你仔细看看 Runnable 的结构:
// 这就是抽象命令
public interface Runnable {
public abstract void run();
}
// 具体命令:实现 Runnable
public class PrintTask implements Runnable {
private String message;
public PrintTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("打印消息: " + message);
}
}
// 调用者:Thread 类
public class Thread {
private Runnable target;
public Thread(Runnable target) {
this.target = target;
}
public void start() {
// 内部会调用 target.run()
}
}
// 客户端使用
public class Client {
public static void main(String[] args) {
Runnable task = new PrintTask("Hello Command!");
Thread thread = new Thread(task);
thread.start();
}
}
你看,Runnable 就是抽象命令,PrintTask 是具体命令,Thread 就是调用者。我在项目中经常用这种方式封装异步任务,尤其是需要把任务提交到线程池的时候——ExecutorService.submit(Runnable) 本质上就是命令模式的应用。
小提示:Java 8 以后,你可以用 Lambda 表达式直接替代匿名内部类,比如 Thread(() -> System.out.println("Hello"))。但底层原理没变,还是命令模式。
命令模式在 Spring JdbcTemplate 中的回调
Spring 的 JdbcTemplate 大量使用了回调机制,而回调的本质就是命令模式。我拿最常用的 query() 方法举例:
// 抽象命令:RowMapper
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}
// 具体命令:将结果集映射为 User 对象
public class UserRowMapper implements RowMapper<User> {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setEmail(rs.getString("email"));
return user;
}
}
// 调用者:JdbcTemplate
public class JdbcTemplate {
public <T> List<T> query(String sql, RowMapper<T> rowMapper) {
Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
List<T> results = new ArrayList<>();
int rowNum = 0;
while (rs.next()) {
// 调用命令的 execute() —— 也就是 mapRow()
results.add(rowMapper.mapRow(rs, rowNum++));
}
return results;
}
}
// 客户端使用
public class UserDao {
private JdbcTemplate jdbcTemplate;
public List<User> findAllUsers() {
String sql = "SELECT * FROM users";
// 传入具体命令
return jdbcTemplate.query(sql, new UserRowMapper());
}
}
这里 RowMapper 就是抽象命令,UserRowMapper 是具体命令,JdbcTemplate 是调用者。JdbcTemplate 负责处理数据库连接、创建 Statement、遍历 ResultSet 这些「模板化」的工作,而「如何把一行数据变成对象」这个变化的部分,通过命令对象传入。
避坑指南:我曾经在项目里看到有人把复杂的业务逻辑直接写在 RowMapper 里,比如调用远程服务或者做大量计算。这其实违背了命令模式的初衷——RowMapper 应该只做「映射」这一件事。如果你需要做复杂处理,建议在 Service 层完成,或者使用更高级的 ORM 框架。
Spring 里类似的回调接口还有很多,比如 ConnectionCallback、StatementCallback、PreparedStatementCallback,它们都是命令模式的具体体现。说白了,Spring 把「模板方法 + 命令模式」玩得炉火纯青——固定流程由模板控制,变化部分通过命令对象注入。
命令模式的实际价值
我总结一下,命令模式在工程中主要解决这几个问题:
- 解耦调用者和接收者:调用者不需要知道谁在干活、怎么干活。
- 支持命令的排队、记录和撤销:因为命令是对象,可以存到队列里,也可以记录日志用于回滚。
- 方便扩展:新增一个命令,只需要写一个新的 ConcreteCommand 类,不需要改现有代码。
我记得有一次做交易系统,需要支持「批量撤销订单」功能。如果用传统方式,每个撤销逻辑散落在各个 Service 里,根本没法统一管理。后来我用命令模式,把每个撤销操作封装成 Command 对象,放到一个 List 里,遍历执行。如果要撤销,就反向遍历调用 undo()。代码清晰多了,维护起来也轻松。
一句话总结:命令模式就是把「做什么」和「谁来做」分开,中间通过一个命令对象来传递。你想想看,这种思想在回调、线程池、事务、消息队列里无处不在。
公众号:蓝海资料掘金营,微信deep3321