← 返回题目列表

如何手写一个命令模式?核心代码怎么设计?

高频 中等 第 11 / 25 题 更新于 2026/07/28
命令模式代码实现Java撤销设计模式

简化版

手写命令模式通常先定义 Command 接口,再实现具体命令类,让命令类持有真正的业务执行者 Receiver,最后由 Invoker 保存并触发命令。需要撤销时,可以在命令接口里增加 undo(),并用栈保存执行历史。

详细版

一个标准命令模式可以按五步写:

  1. 定义命令接口;
  2. 定义真正执行业务的 Receiver;
  3. 为每个操作实现具体命令;
  4. 让 Invoker 持有命令并触发;
  5. 如果需要撤销,记录历史命令并实现 undo()

简化代码:

interface Command {
    void execute();
    void undo();
}

class LightOnCommand implements Command {
    private final Light light;

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

    public void execute() {
        light.on();
    }

    public void undo() {
        light.off();
    }
}

命令模式的实现重点不是代码难度,而是职责划分:Invoker 不写业务,Command 封装请求,Receiver 负责真实动作。

完整版教学

一、定义命令接口

先抽象出统一命令:

interface Command {
    void execute();
    void undo();
}

如果业务不需要撤销,undo() 可以不定义。但面试里命令模式经常和撤销重做一起考,所以加上更完整。

二、定义接收者 Receiver

Receiver 是真正执行业务的对象。

class Light {
    void on() {
        System.out.println("灯打开了");
    }

    void off() {
        System.out.println("灯关闭了");
    }
}

这里的 Light 可以替换成真实项目里的 Service、Repository、SDK Client 或领域对象。

三、实现具体命令

打开灯命令:

class LightOnCommand implements Command {
    private final Light light;

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

    public void execute() {
        light.on();
    }

    public void undo() {
        light.off();
    }
}

关闭灯命令:

class LightOffCommand implements Command {
    private final Light light;

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

    public void execute() {
        light.off();
    }

    public void undo() {
        light.on();
    }
}

每个命令都知道自己怎么执行,也知道如何反向恢复。

四、设计调用者 Invoker

Invoker 只负责保存和触发命令。

class RemoteControl {
    private Command command;

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

    void pressButton() {
        command.execute();
    }
}

这个遥控器可以控制任意命令,而不是写死控制灯。

五、加入撤销栈

如果要支持撤销,可以记录最近执行过的命令。

class RemoteControl {
    private Command command;
    private final Deque<Command> history = new ArrayDeque<>();

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

    void pressButton() {
        command.execute();
        history.push(command);
    }

    void undo() {
        if (!history.isEmpty()) {
            history.pop().undo();
        }
    }
}

这就是很多编辑器撤销功能的基础思路。

六、工程实现里要关注失败处理

真实项目中 execute() 可能失败。比如发起支付、扣库存、调用远程接口,都可能出现异常。

这时要考虑:

  • 执行失败的命令是否入历史;
  • undo() 是否一定可靠;
  • 是否需要幂等键;
  • 是否需要重试;
  • 是否要记录命令执行日志;
  • 是否要持久化命令状态。

命令模式只是结构,不能替代事务、消息可靠性和异常治理。

七、常见误区与追问

验收命令模式不能只看类名,而要验证调用者是否只依赖 Command、接收者是否保留业务能力。以“开灯、关灯”2 个命令为例,新增“调暗”时应只新增命令实现,不修改按钮的触发流程。若 Invoker 仍按命令类型写 if/else,对象化请求并没有真正带来扩展性。

检查维度判定依据
新增请求新增 Command,Invoker 不变
业务执行委托 Receiver,不塞进 Invoker

记忆钩子:Invoker 管“何时触发”,Command 管“触发什么”,Receiver 管“具体怎么做”。

  • 误区:Command 必须是无状态对象。 撤销、审计或重试往往要求它保存参数与执行前快照;关键是状态边界清晰。
  • 追问:为什么不让按钮直接调用 Receiver? 直接调用会把界面控件与业务接口绑死,也无法统一排队、记录和撤销。
  • 误区:execute 中不能写任何业务逻辑。 它可以做参数组织与调用编排,但核心领域规则仍应留在 Receiver 或领域服务。
  • 追问:命令对象应该复用还是每次新建? 无状态命令可以复用;携带请求参数、快照或执行结果的命令通常一次请求一个实例。
  • 追问:如何证明实现符合开闭原则? 加入第 3 种命令并运行原有 Invoker 测试,若无需修改分派代码才算成立。

八、加强记忆

手写命令模式抓住三件事:Command 定义统一操作,ConcreteCommand 持有 Receiver 并封装请求,Invoker 只负责触发命令。需要撤销时给命令补 undo(),并用历史栈管理已执行命令。