命令模式:把请求打包成对象

命令模式,说白了就是把「你要做什么」这件事,封装成一个独立的对象。我刚开始接触这个模式时,觉得有点绕——好好的方法调用不用,非得包一层?后来在项目中踩过坑,才明白它的价值。

举个例子。你写了一个遥控器程序,有开灯、关灯、开空调、关空调四个按钮。最直接的做法,就是每个按钮的点击事件里直接调用对应的方法。但问题来了——如果我想加一个「撤销」功能呢?或者我想把一系列操作录制成宏,一键执行?这时候,命令模式就派上用场了。

定义与核心思想

命令模式(Command Pattern)属于行为型设计模式。它将一个请求封装为一个对象,从而让你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

嗯,这里要注意:核心就是把「行为调用者」和「行为执行者」彻底解耦。调用者不需要知道谁去执行、怎么执行,它只需要发出命令就行了。

结构组成

命令模式的结构其实很清晰,就四个角色:

角色 职责 我见过的常见实现
Command(抽象命令) 声明执行操作的接口 通常是一个接口或抽象类,包含 execute() 方法
ConcreteCommand(具体命令) 绑定接收者与具体操作 持有接收者对象的引用,在 execute() 中调用接收者的方法
Invoker(调用者) 要求命令对象执行请求 比如遥控器、菜单项、任务队列
Receiver(接收者) 真正执行业务逻辑的对象 比如灯、空调、数据库连接

关键点:调用者只跟 Command 接口打交道,不关心具体是谁在执行。接收者也可以完全不知道命令对象的存在。

核心逻辑流程图

下面这张图,是我自己画的一个命令模式执行流程。你看一眼就明白了:

命令模式执行流程 Invoker (调用者/遥控器) Command (抽象命令接口) ConcreteCommand (具体命令) Receiver (接收者/执行者) 调用 execute() 实现 调用 action() 💡 流程说明: 1. Invoker 持有 Command 引用,调用 execute() 2. ConcreteCommand 持有 Receiver 引用,在 execute() 中调用 receiver.action()

代码实现

我用一个智能家居的遥控器场景来演示。你想想看,这应该是命令模式最经典的例子了。

// 1. 接收者:灯
class Light {
    public void turnOn() {
        System.out.println("灯亮了");
    }
    public void turnOff() {
        System.out.println("灯灭了");
    }
}

// 2. 抽象命令接口
interface Command {
    void execute();
    void undo();  // 支持撤销
}

// 3. 具体命令:开灯
class LightOnCommand implements Command {
    private Light light;

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

    @Override
    public void execute() {
        light.turnOn();
    }

    @Override
    public void undo() {
        light.turnOff();
    }
}

// 4. 具体命令:关灯
class LightOffCommand implements Command {
    private Light light;

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

    @Override
    public void execute() {
        light.turnOff();
    }

    @Override
    public void undo() {
        light.turnOn();
    }
}

// 5. 调用者:遥控器
class RemoteControl {
    private Command slot;
    private Command lastCommand;  // 记录上一条命令用于撤销

    public void setCommand(Command command) {
        this.slot = command;
    }

    public void pressButton() {
        slot.execute();
        lastCommand = slot;
    }

    public void pressUndo() {
        if (lastCommand != null) {
            lastCommand.undo();
        }
    }
}

// 6. 客户端使用
public class Client {
    public static void main(String[] args) {
        Light livingRoomLight = new Light();
        Command lightOn = new LightOnCommand(livingRoomLight);
        Command lightOff = new LightOffCommand(livingRoomLight);

        RemoteControl remote = new RemoteControl();

        // 开灯
        remote.setCommand(lightOn);
        remote.pressButton();  // 输出:灯亮了

        // 撤销
        remote.pressUndo();    // 输出:灯灭了
    }
}

我的经验:在实际项目中,我习惯把 undo 操作也设计成命令模式的一部分。这样撤销功能就不需要额外维护状态栈了,每个命令自己知道怎么回退。

优缺点分析

任何模式都有两面性。命令模式也不例外。

优点 缺点
彻底解耦调用者与接收者 每增加一个命令就要新建一个类,类数量膨胀
容易实现撤销和重做 如果命令对象持有大量状态,内存开销会增大
支持命令的排队、记录、日志 对于简单场景,过度设计
可以组合命令形成宏命令 调试时命令链可能变长,追踪困难

避坑指南:我曾经在一个项目中,把每个数据库操作都封装成命令对象。结果一个页面加载要创建几十个命令实例,内存直接飙上去。后来才意识到——命令模式适合「需要记录、撤销、排队」的场景,不是所有方法调用都要包一层。

应用场景

什么时候该用命令模式?我总结了几个典型场景:

  • 需要支持撤销/重做:比如文本编辑器的 Ctrl+Z、画图软件的撤销操作
  • 需要记录操作日志:比如金融系统的交易记录,每个操作都要持久化
  • 需要异步执行或任务队列:比如线程池的任务提交,每个任务就是一个命令对象
  • 需要宏命令/组合命令:比如游戏中的连招系统,一键执行多个操作
  • 需要将请求参数化:比如菜单系统,每个菜单项对应一个命令对象

我记得有一次做电商后台的订单处理系统,需要支持「批量操作」——比如批量发货、批量退款。每个操作都要记录日志,还要支持部分撤销。用命令模式实现后,代码结构清晰了很多,后来加新功能也只需要新增一个命令类,完全不影响现有代码。

与策略模式的区别

很多初学者会把命令模式和策略模式搞混。我简单说一下:

  • 策略模式:关注「怎么做」,不同的算法可以互相替换。比如排序算法,你可以选快排或归并。
  • 命令模式:关注「做什么」,把请求封装成对象,支持排队、撤销、记录。比如遥控器按钮,按了就执行。

说白了,策略模式是算法的封装,命令模式是请求的封装。两者目的完全不同。

一句话总结:命令模式就是把「请求」变成「对象」,让请求可以被传递、存储、撤销、组合。它解决的核心问题是——如何让调用者不需要知道接收者的任何细节。

嗯,命令模式就讲到这里。如果你在实际项目中用过这个模式,欢迎交流你的踩坑经历。


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