设计模式-命令模式

文章目录

  • 一、概述
    • [1.1 结构与角色](#1.1 结构与角色)
    • [1.2 适用场景](#1.2 适用场景)
  • 二、实现方式
    • [2.1 基本命令](#2.1 基本命令)
    • [2.2 宏命令](#2.2 宏命令)
    • [2.3 撤销与恢复](#2.3 撤销与恢复)
  • 三、总结

一、概述

在软件开发中,经常会遇到这样的场景:需要对某个操作进行"请求化"管理------将请求封装为对象,从而支持参数化、队列化、日志化 ,以及撤销/恢复 等功能。例如,遥控器上每个按键对应一个操作,但遥控器本身不需要知道操作的具体实现;文本编辑器的撤销功能需要记录每一步操作;任务调度系统需要将任务放入队列异步执行。如果让调用者直接调用接收者的方法,就会产生紧耦合------调用者必须知道接收者的所有细节,且无法对操作进行统一管理:
直接调用 open()
直接调用 turnOn()
直接调用 start()
直接调用 play()
调用者
电视
电灯
空调
音响
调用者与每个设备强耦合,新增设备需修改调用者

命令模式(Command Pattern)正是为了解决这个问题而诞生的------它将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。命令模式将"发出请求的对象"和"执行请求的对象"完全解耦。

生活中的命令模式例子:

  • 餐厅点餐:顾客(Client)把点餐单(Command)交给服务员(Invoker),服务员把点餐单递给厨师(Receiver),厨师根据点餐单做菜。顾客不需要直接和厨师沟通,服务员也只负责传递点餐单
  • 遥控器:遥控器(Invoker)上的每个按键对应一个命令对象(Command),按下按键时执行对应的命令,遥控器本身不关心命令具体做什么
  • 文本编辑器撤销:每次编辑操作被封装为一个命令对象,编辑器的撤销栈保存所有命令,撤销时按相反顺序执行命令的逆向操作
  • 任务队列:线程池中的任务(Runnable / Callable)就是命令对象,线程池只管调度执行,不关心任务的具体逻辑

核心:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作

1.1 结构与角色

命令模式包含以下角色:
持有并调用
实现
调用
设置命令
Client 客户端
Receiver 接收者
ConcreteCommand 具体命令
Invoker 调用者
Command 命令接口

  • Command(命令接口) :声明执行操作的接口,通常包含 execute() 方法,有时也包含 undo() 方法
  • ConcreteCommand(具体命令):实现命令接口,将一个接收者对象绑定于一个动作,调用接收者相应的操作
  • Invoker(调用者) :持有命令对象,在某个时间点调用命令对象的 execute() 方法执行请求
  • Receiver(接收者):知道如何实施与执行一个请求相关的具体操作,任何类都可能作为一个接收者
  • Client(客户端):创建一个具体命令对象并设定它的接收者,同时将命令对象设置给调用者

1.2 适用场景

  • 需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
  • 需要在不同的时间指定请求、将请求排队和执行请求
  • 需要支持撤销操作和恢复操作
  • 需要支持宏命令------将一组操作组合为一个复合命令
  • 需要将系统的操作记录到日志中,以便在系统崩溃时重新执行

二、实现方式

命令模式的核心实现思路是:将"动作"封装为独立的命令对象,命令对象持有接收者的引用,调用者通过命令接口与具体命令交互,从而将"请求的发起"与"请求的执行"彻底解耦。

2.1 基本命令

以"智能家居遥控器"为例,遥控器上有多个按钮,每个按钮控制一种家电(电视、电灯、音响),遥控器只需知道"执行"操作,无需了解设备的具体控制细节:
持有
实现
实现
实现
实现
调用
调用
客户端
TV 电视
Light 电灯
RemoteControl 遥控器
Command 命令接口
TVOnCommand 开电视
TVOffCommand 关电视
LightOnCommand 开灯
LightOffCommand 关灯

(1)命令接口

java 复制代码
/**
 * 命令接口
 * 所有具体命令必须实现此接口
 */
public interface Command {

    /**
     * 执行命令
     */
    void execute();
}

(2)接收者------电视

java 复制代码
/**
 * 接收者:电视
 * 提供电视的具体操作
 */
public class TV {

    private final String location;

    public TV(String location) {
        this.location = location;
    }

    public void on() {
        System.out.println(location + " 电视:已打开");
    }

    public void off() {
        System.out.println(location + " 电视:已关闭");
    }

    public void setChannel(int channel) {
        System.out.println(location + " 电视:切换到 " + channel + " 频道");
    }
}

(3)接收者------电灯

java 复制代码
/**
 * 接收者:电灯
 * 提供电灯的具体操作
 */
public class Light {

    private final String location;

    public Light(String location) {
        this.location = location;
    }

    public void on() {
        System.out.println(location + " 电灯:已打开");
    }

    public void off() {
        System.out.println(location + " 电灯:已关闭");
    }
}

(4)具体命令------开电视命令

java 复制代码
/**
 * 具体命令:打开电视
 * 封装了打开电视的操作
 */
public class TVOnCommand implements Command {

    private final TV tv;

    public TVOnCommand(TV tv) {
        this.tv = tv;
    }

    @Override
    public void execute() {
        tv.on();
        tv.setChannel(1);
    }
}

(5)具体命令------关电视命令

java 复制代码
/**
 * 具体命令:关闭电视
 * 封装了关闭电视的操作
 */
public class TVOffCommand implements Command {

    private final TV tv;

    public TVOffCommand(TV tv) {
        this.tv = tv;
    }

    @Override
    public void execute() {
        tv.off();
    }
}

(6)具体命令------开灯命令

java 复制代码
/**
 * 具体命令:打开电灯
 * 封装了打开电灯的操作
 */
public class LightOnCommand implements Command {

    private final Light light;

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

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

(7)具体命令------关灯命令

java 复制代码
/**
 * 具体命令:关闭电灯
 * 封装了关闭电灯的操作
 */
public class LightOffCommand implements Command {

    private final Light light;

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

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

(8)调用者------遥控器

java 复制代码
/**
 * 调用者:遥控器
 * 持有命令对象,负责在合适时机调用命令
 */
public class RemoteControl {

    /** 最多 7 个按钮插槽 */
    private static final int SLOT_COUNT = 7;

    /** 每个插槽对应一个命令(开) */
    private final Command[] onCommands  = new Command[SLOT_COUNT];
    /** 每个插槽对应一个命令(关) */
    private final Command[] offCommands = new Command[SLOT_COUNT];

    /**
     * 为某个插槽设置开/关命令
     *
     * @param slot      插槽编号(0~6)
     * @param onCommand 打开命令
     * @param offCommand 关闭命令
     */
    public void setCommand(int slot, Command onCommand, Command offCommand) {
        if (slot < 0 || slot >= SLOT_COUNT) {
            throw new IllegalArgumentException("插槽编号越界:" + slot);
        }
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }

    /**
     * 按下"开"按钮
     *
     * @param slot 插槽编号
     */
    public void pressOnButton(int slot) {
        if (onCommands[slot] != null) {
            onCommands[slot].execute();
        } else {
            System.out.println("插槽 " + slot + " 未设置命令");
        }
    }

    /**
     * 按下"关"按钮
     *
     * @param slot 插槽编号
     */
    public void pressOffButton(int slot) {
        if (offCommands[slot] != null) {
            offCommands[slot].execute();
        } else {
            System.out.println("插槽 " + slot + " 未设置命令");
        }
    }
}

(9)客户端调用

java 复制代码
public class CommandDemo {
    public static void main(String[] args) {
        // 创建接收者(家电设备)
        TV livingRoomTV = new TV("客厅");
        Light kitchenLight = new Light("厨房");

        // 创建命令对象
        Command tvOn     = new TVOnCommand(livingRoomTV);
        Command tvOff    = new TVOffCommand(livingRoomTV);
        Command lightOn  = new LightOnCommand(kitchenLight);
        Command lightOff = new LightOffCommand(kitchenLight);

        // 创建调用者(遥控器),绑定命令
        RemoteControl remote = new RemoteControl();
        remote.setCommand(0, tvOn,    tvOff);     // 插槽0:控制电视
        remote.setCommand(1, lightOn, lightOff);  // 插槽1:控制电灯

        // 使用遥控器
        remote.pressOnButton(0);
        // 客厅 电视:已打开
        // 客厅 电视:切换到 1 频道

        remote.pressOnButton(1);
        // 厨房 电灯:已打开

        remote.pressOffButton(1);
        // 厨房 电灯:已关闭

        remote.pressOffButton(0);
        // 客厅 电视:已关闭
    }
}

关键点 :遥控器只依赖 Command 接口,无需知道电视、电灯等具体设备的存在。新增一个家电(如空调),只需新增对应的命令类并设置到遥控器的空闲插槽即可,遥控器代码无需任何修改,符合开闭原则

2.2 宏命令

宏命令(Macro Command)是一种特殊的命令,它内部持有一组命令的集合,执行宏命令时会依次执行其中的每个子命令。宏命令本质上也是实现了 Command 接口的类,因此可以像普通命令一样被调用者使用,也可以嵌套(宏命令中包含另一个宏命令)。

以"智能家居场景模式"为例,一键"回家模式"会自动打开客厅灯、打开电视、开启空调:
实现
持有
持有
持有
持有
execute
客户端
MacroCommand 宏命令
Command 接口
TVOnCommand 开电视
LightOnCommand 开灯
ACOnCommand 开空调
RemoteControl 遥控器

(1)宏命令

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * 宏命令
 * 将多个命令组合为一个复合命令,按添加顺序依次执行
 */
public class MacroCommand implements Command {

    private final List<Command> commands = new ArrayList<>();

    /**
     * 添加子命令
     *
     * @param command 子命令
     * @return 当前宏命令(支持链式添加)
     */
    public MacroCommand addCommand(Command command) {
        commands.add(command);
        return this;
    }

    /**
     * 移除子命令
     *
     * @param command 子命令
     */
    public void removeCommand(Command command) {
        commands.remove(command);
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
}

(2)接收者------空调(新增设备)

java 复制代码
/**
 * 接收者:空调
 */
public class AC {

    private final String location;

    public AC(String location) {
        this.location = location;
    }

    public void on() {
        System.out.println(location + " 空调:已开启(制冷 26°C)");
    }

    public void off() {
        System.out.println(location + " 空调:已关闭");
    }
}

(3)具体命令------开空调命令

java 复制代码
/**
 * 具体命令:开启空调
 */
public class ACOnCommand implements Command {

    private final AC ac;

    public ACOnCommand(AC ac) {
        this.ac = ac;
    }

    @Override
    public void execute() {
        ac.on();
    }
}

(4)客户端调用------宏命令

java 复制代码
public class MacroCommandDemo {
    public static void main(String[] args) {
        // 创建接收者
        TV livingRoomTV = new TV("客厅");
        Light livingRoomLight = new Light("客厅");
        AC livingRoomAC = new AC("客厅");

        // 创建宏命令:一键回家模式
        MacroCommand homeMode = new MacroCommand();
        homeMode.addCommand(new TVOnCommand(livingRoomTV))
                .addCommand(new LightOnCommand(livingRoomLight))
                .addCommand(new ACOnCommand(livingRoomAC));

        // 创建遥控器,将宏命令绑定到插槽
        RemoteControl remote = new RemoteControl();
        remote.setCommand(0, homeMode, new NoCommand());  // 插槽0:回家模式

        // 按一下,全部开启
        remote.pressOnButton(0);
        // 客厅 电视:已打开
        // 客厅 电视:切换到 1 频道
        // 客厅 电灯:已打开
        // 客厅 空调:已开启(制冷 26°C)
    }
}

(5)空命令------避免空指针检查

java 复制代码
/**
 * 空命令(Null Object 模式)
 * 什么都不做,避免遥控器中判空,简化代码
 */
public class NoCommand implements Command {

    @Override
    public void execute() {
        // 空实现
    }
}

关键点 :宏命令实现了 Command 接口,因此它可以像普通命令一样被遥控器持有和执行,这就是命令模式的强大之处------命令对象可以被任意组合、嵌套,形成树形结构。

2.3 撤销与恢复

命令模式最经典的应用之一就是撤销(Undo)恢复(Redo) 。通过在命令接口中增加 undo() 方法,每个具体命令在 execute() 时保存必要的前置状态,undo() 时恢复状态。

以"文本编辑器"为例,支持输入、删除操作的撤销与恢复:
持有
维护栈
实现
实现
调用
调用
Editor 文本编辑器
CommandHistory 命令历史
undoStack / redoStack
WriteCommand 写入
Command 接口
DeleteCommand 删除
Document 文档

(1)带撤销的命令接口

java 复制代码
/**
 * 可撤销的命令接口
 * 在基本 Command 的基础上增加 undo() 方法
 */
public interface UndoableCommand {

    /**
     * 执行命令
     */
    void execute();

    /**
     * 撤销命令(逆向操作)
     */
    void undo();
}

(2)接收者------文档

java 复制代码
/**
 * 接收者:文档
 * 维护文本内容和光标位置
 */
public class Document {

    private final StringBuilder content = new StringBuilder();
    private int cursorPosition = 0;

    /**
     * 在光标位置插入文本
     *
     * @param text 要插入的文本
     */
    public void insert(String text) {
        if (text == null || text.isEmpty()) {
            return;
        }
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
    }

    /**
     * 删除光标位置前的指定长度文本
     *
     * @param length 删除长度
     * @return 被删除的文本(用于撤销)
     */
    public String delete(int length) {
        if (length <= 0 || cursorPosition == 0) {
            return "";
        }
        int start = Math.max(0, cursorPosition - length);
        String deleted = content.substring(start, cursorPosition);
        content.delete(start, cursorPosition);
        cursorPosition = start;
        return deleted;
    }

    public String getContent() {
        return content.toString();
    }

    public int getCursorPosition() {
        return cursorPosition;
    }

    public void setCursorPosition(int position) {
        if (position < 0 || position > content.length()) {
            throw new IllegalArgumentException("光标位置越界:" + position);
        }
        this.cursorPosition = position;
    }
}

(3)具体命令------写入命令

java 复制代码
/**
 * 具体命令:写入文本
 * 执行时插入文本并记录位置,撤销时删除刚插入的文本
 */
public class WriteCommand implements UndoableCommand {

    private final Document document;
    private final String text;
    private int insertPosition;

    public WriteCommand(Document document, String text) {
        this.document = document;
        this.text = text;
    }

    @Override
    public void execute() {
        // 保存插入前光标位置
        insertPosition = document.getCursorPosition();
        document.insert(text);
    }

    @Override
    public void undo() {
        // 撤销:定位到插入位置,删除刚插入的文本
        document.setCursorPosition(insertPosition + text.length());
        document.delete(text.length());
    }
}

(4)具体命令------删除命令

java 复制代码
/**
 * 具体命令:删除文本
 * 执行时删除指定长度文本并保存内容,撤销时重新插入
 */
public class DeleteCommand implements UndoableCommand {

    private final Document document;
    private final int length;
    /** 被删除的文本(用于撤销时恢复) */
    private String deletedText;

    public DeleteCommand(Document document, int length) {
        this.document = document;
        this.length = length;
    }

    @Override
    public void execute() {
        deletedText = document.delete(length);
    }

    @Override
    public void undo() {
        if (deletedText != null && !deletedText.isEmpty()) {
            document.insert(deletedText);
        }
    }
}

(5)命令历史------管理撤销/恢复栈

java 复制代码
import java.util.Stack;

/**
 * 命令历史管理器
 * 维护撤销栈和恢复栈,支持多步撤销与恢复
 */
public class CommandHistory {

    /** 撤销栈:记录已执行的命令 */
    private final Stack<UndoableCommand> undoStack = new Stack<>();

    /** 恢复栈:记录已撤销的命令 */
    private final Stack<UndoableCommand> redoStack = new Stack<>();

    /**
     * 执行命令并将其记录到撤销栈
     *
     * @param command 要执行的命令
     */
    public void executeCommand(UndoableCommand command) {
        command.execute();
        undoStack.push(command);
        // 执行新命令后清空恢复栈(不能恢复已过期的操作)
        redoStack.clear();
    }

    /**
     * 撤销最近一次操作
     */
    public void undo() {
        if (!undoStack.isEmpty()) {
            UndoableCommand command = undoStack.pop();
            command.undo();
            redoStack.push(command);
        } else {
            System.out.println("没有可撤销的操作");
        }
    }

    /**
     * 恢复最近一次撤销
     */
    public void redo() {
        if (!redoStack.isEmpty()) {
            UndoableCommand command = redoStack.pop();
            command.execute();
            undoStack.push(command);
        } else {
            System.out.println("没有可恢复的操作");
        }
    }
}

(6)客户端调用------撤销与恢复

java 复制代码
public class UndoDemo {
    public static void main(String[] args) {
        Document document = new Document();
        CommandHistory history = new CommandHistory();

        // 执行写入操作
        history.executeCommand(new WriteCommand(document, "Hello"));
        history.executeCommand(new WriteCommand(document, " World"));
        System.out.println("当前内容:" + document.getContent());
        // 当前内容:Hello World

        // 撤销一步
        history.undo();
        System.out.println("撤销后:" + document.getContent());
        // 撤销后:Hello

        // 再撤销一步
        history.undo();
        System.out.println("再撤销:" + document.getContent());
        // 再撤销:

        // 恢复一步
        history.redo();
        System.out.println("恢复后:" + document.getContent());
        // 恢复后:Hello

        // 执行删除操作
        history.executeCommand(new DeleteCommand(document, 5));
        System.out.println("删除后:" + document.getContent());
        // 删除后:

        // 撤销删除
        history.undo();
        System.out.println("撤销删除:" + document.getContent());
        // 撤销删除:Hello
    }
}

关键点 :撤销机制的核心在于每个命令对象在执行时保存了足够的前置状态 ------WriteCommand 记录了插入位置,DeleteCommand 保存了被删除的文本。撤销时只需调用 undo() 逆向操作即可。CommandHistory 管理两个栈(undoStack 和 redoStack),实现多步撤销与恢复。


三、总结

命令模式是行为型设计模式中非常实用的一种,它将"请求"封装为对象,使请求的发送者与接收者完全解耦,并天然支持撤销/恢复、宏命令、任务队列等高级特性。本文围绕命令模式的核心思想,从基本命令到宏命令再到撤销恢复进行了系统讲解:

章节 核心内容
一、概述 介绍了命令模式的定义、结构和适用场景,通过餐厅点餐、遥控器等生活案例帮助理解
二、实现方式 以智能家居遥控器演示基本命令,以场景模式演示宏命令,以文本编辑器演示多步撤销与恢复

命令模式的核心优势:

  • 解耦调用者与接收者:调用者只需知道命令接口,无需了解接收者的具体实现
  • 易于扩展:新增命令只需新增命令类,无需修改调用者和其他命令,符合开闭原则
  • 支持组合:通过宏命令将多个命令组合为复合命令,甚至支持嵌套
  • 天然支持撤销/恢复:命令对象可保存状态,轻松实现多步撤销与恢复
  • 支持命令队列与日志:命令对象可被序列化、持久化,实现任务调度和崩溃恢复
优点 缺点
调用者与接收者完全解耦 类数量膨胀------每个操作都需要一个命令类
易于扩展,新增命令无需修改已有代码 命令对象可能持有大量状态,内存开销较大
支持宏命令,可将命令组合与嵌套 撤销/恢复的实现需要命令对象保存前置状态
天然支持撤销/恢复、队列、日志 调用链变长,调试复杂度增加
命令对象可序列化,支持持久化与分布式 对于简单操作,引入命令模式可能过度设计

在实际开发中,命令模式广泛应用于:Java 线程池中的 Runnable / Callable(命令对象)、Swing 中的 Action 接口、Spring 的 JdbcTemplate 回调、消息队列中的任务封装、文本编辑器的撤销/恢复、IDE 的宏录制与回放等。掌握命令模式,能帮助你构建灵活、可扩展、支持高级操作管理能力的系统。

相关推荐
benpaodeDD4 小时前
视频49——设计模式之责任链模式
设计模式·责任链模式
雪度娃娃4 小时前
行为型设计模式——迭代器模式
c++·设计模式·迭代器模式
踩着两条虫4 小时前
可视化设计器组件系统:从交互核心到 AI 智能代理的落地实践
开发语言·前端·人工智能·低代码·设计模式·架构
nnsix17 小时前
设计模式 - 模板方法模式 笔记
笔记·设计模式·模板方法模式
likerhood21 小时前
设计模式-装饰器模式(java)
java·设计模式·装饰器模式
青瓦梦滋1 天前
C++特殊类设计(设计模式)和类型转换
c++·设计模式
geovindu1 天前
python: Monitor Pattern
开发语言·python·设计模式·监控模式
workflower1 天前
人工智能全球治理
大数据·人工智能·设计模式·机器人·动态规划
workflower1 天前
AI灵活高效的智慧用能核心场景
大数据·人工智能·设计模式·机器人·动态规划