命令模式(Command):把请求封装成对象

命令模式,说白了就是把「你要干什么」这件事,包装成一个对象。你想想看,在代码里我们经常要调用某个方法,比如 orderService.submit(order),这是直接调用。但有时候,我们希望把「提交订单」这个动作本身作为一个参数传来传去,或者放到队列里排队执行,甚至支持撤销——这时候,命令模式就派上用场了。

我个人习惯把命令模式理解为「任务票」。你拿着票,不一定马上执行,可以攒着、可以排队、甚至可以退票。这种灵活性,在很多场景下非常有用。

命令模式的结构

命令模式的核心角色有四个:

  • Command(抽象命令):声明执行操作的接口,通常就一个 execute() 方法。
  • ConcreteCommand(具体命令):实现 Command 接口,内部持有接收者对象,在 execute() 中调用接收者的具体方法。
  • Receiver(接收者):真正干活的对象,知道如何实现具体的业务逻辑。
  • Invoker(调用者/请求者):持有命令对象,在某个时机调用命令的 execute() 方法。

我画了一张图,帮你理清它们之间的关系:

Invoker 调用者 <<interface>> Command + execute() ConcreteCommand - receiver: Receiver + execute() Receiver + action() 持有 调用 Client 创建 设置命令

你看这个图,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 里类似的回调接口还有很多,比如 ConnectionCallbackStatementCallbackPreparedStatementCallback,它们都是命令模式的具体体现。说白了,Spring 把「模板方法 + 命令模式」玩得炉火纯青——固定流程由模板控制,变化部分通过命令对象注入。

命令模式的实际价值

我总结一下,命令模式在工程中主要解决这几个问题:

  • 解耦调用者和接收者:调用者不需要知道谁在干活、怎么干活。
  • 支持命令的排队、记录和撤销:因为命令是对象,可以存到队列里,也可以记录日志用于回滚。
  • 方便扩展:新增一个命令,只需要写一个新的 ConcreteCommand 类,不需要改现有代码。

我记得有一次做交易系统,需要支持「批量撤销订单」功能。如果用传统方式,每个撤销逻辑散落在各个 Service 里,根本没法统一管理。后来我用命令模式,把每个撤销操作封装成 Command 对象,放到一个 List 里,遍历执行。如果要撤销,就反向遍历调用 undo()。代码清晰多了,维护起来也轻松。

一句话总结:命令模式就是把「做什么」和「谁来做」分开,中间通过一个命令对象来传递。你想想看,这种思想在回调、线程池、事务、消息队列里无处不在。


公众号:蓝海资料掘金营,微信deep3321