当前位置: 首页 > backend >正文

Java 设计模式心法之第21篇 - 命令 (Command) - 将请求封装成对象,实现操作解耦与扩展

好的,我们继续《Java 设计模式心法》第四卷:运筹篇,接下来是系列的第二十一章,深入探讨命令模式——一种将请求封装成对象的行为型模式。


Java 设计模式心法:命令 (Command) - 将请求封装成对象,实现操作解耦与扩展

系列: Java 设计模式心法 | 卷: 运筹篇 (行为型模式) | 作者: [你的名字/笔名]

摘要: 在软件交互中,我们常常需要触发某个动作或操作。通常的做法是请求发起者(客户端)直接调用接收者(执行者)的方法。但这种方式存在弊端:发起者与接收者紧密耦合,难以对请求本身进行管理(如排队、撤销、记录日志)。有没有一种方法能将**“要做什么”(请求)本身封装成一个独立的对象**,从而解耦发起者和执行者,并赋予请求本身更多的可能性?本文将带你深入理解行为型模式中的“任务调度官”——命令模式。我们将揭示它如何通过将一个请求或操作包装成一个命令对象 (Command),使得我们可以用不同的请求对客户端进行参数化,将请求放入队列、记录请求日志,以及支持可撤销的操作。从 GUI 按钮的 ActionListener 到线程池的任务提交 (Runnable),再到事务处理,命令模式的应用极其广泛。


一、问题的提出:当“直接指挥”遭遇“管理失控”与“耦合僵局”

想象一下你正在设计一个智能家居的遥控器 (Remote Control - Client):

  • 遥控器上有多个按钮。
  • 每个按钮需要控制不同的家电(灯 Light, 风扇 Fan, 电视 TV, 音响 Stereo 等 - Receivers)。
  • 按下按钮时,需要执行相应的操作(开灯 light.on(), 关风扇 fan.off(), 电视换台 tv.setChannel(), 音响调音量 stereo.setVolume() 等)。

如果我们在遥控器的按钮事件处理代码中,直接 new 对应的家电对象并调用其方法:

// 糟糕的设计:遥控器直接依赖并调用具体家电的方法
class RemoteControlBad {Light livingRoomLight = new Light("客厅");Fan ceilingFan = new Fan("吊扇");// ... 其他家电public void button1Pressed() {System.out.println("按钮1 按下 -> 开客厅灯");livingRoomLight.on();}public void button2Pressed() {System.out.println("按钮2 按下 -> 关吊扇");ceilingFan.off();}public void button3Pressed() {// 如果按钮3要控制电视换台?需要 new TV()...// 如果按钮1想改成关灯?需要修改 button1Pressed() 方法...}// ... 每个按钮都需要硬编码一个操作 ...// 如果想实现撤销上一步操作?几乎不可能!// 如果想将操作记录到日志?需要在每个 buttonXPressed 方法里加日志代码...// 如果想将操作放入队列延迟执行?也很困难...
}

这种直接调用的方式存在严重问题:

  1. 高度耦合 (High Coupling): 遥控器 (RemoteControlBad) 与所有具体的家电类 (Light, Fan, TV 等) 产生了紧密的依赖关系。遥控器必须知道每个家电的具体类名和方法名。
  2. 违反开闭原则 (OCP):
    • 增加新家电或新操作: 需要修改 RemoteControlBad 类,添加新的家电引用和按钮处理方法。
    • 修改按钮功能: 需要直接修改对应的 buttonXPressed 方法。
  3. 难以扩展高级功能:
    • 撤销/重做 (Undo/Redo): 无法轻易实现撤销上一步操作的功能,因为操作的上下文信息(比如关灯前灯是开着的)没有被保存。
    • 请求排队/宏命令 (Queueing/Macro Commands): 难以将多个操作组合成一个序列(宏命令)或者将请求放入队列异步执行。
    • 日志记录/事务 (Logging/Transactions): 需要在每个按钮处理方法中重复添加日志代码。实现跨多个操作的事务也变得复杂。

我们需要一种机制,能够将**“按下按钮后要执行的那个操作”本身抽象出来,变成一个可以被传递、存储、管理的对象,从而解耦**遥控器(请求发起者)和家电(请求接收者)。

二、请求对象化:命令模式的核心定义与意图

命令模式 (Command Pattern) 通过将一个请求封装为一个对象,从而让你能够参数化客户端(用不同的请求配置不同的按钮)、对请求进行排队记录请求日志,以及支持可撤销的操作。

GoF 的经典意图描述是:“将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。”

其核心思想在于引入命令对象 (Command) 作为中间层:

  1. 定义命令接口 (Command Interface): 定义一个接口,通常包含一个 execute() 方法,用于执行具体的操作。有时还会包含一个 undo() 方法(用于支持撤销)。
  2. 创建具体命令类 (Concrete Command): 为每一个需要封装的操作创建一个实现了 Command 接口的具体命令类。
    • 持有接收者引用 (Holds Receiver Reference): 具体命令类内部通常会持有一个指向**真正执行操作的对象(接收者 Receiver)**的引用。
    • 实现 execute() 方法:execute() 方法中,调用接收者对象的相应方法来完成具体的操作。
    • (可选)实现 undo() 方法: 如果需要支持撤销,undo() 方法需要包含恢复到 execute() 执行之前状态的逻辑。这可能需要命令对象在执行 execute() 时保存一些状态信息。
  3. 接收者 (Receiver): 知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。它是真正干活的对象。
  4. 调用者 (Invoker):
    • 持有一个或多个 Command 对象的引用。
    • 在需要时调用 Command 对象的 execute() 方法来发起请求。
    • 调用者完全不知道接收者的存在,它只与 Command 接口交互。
    • 调用者可以负责命令的管理,如存储命令历史(用于撤销)、将命令放入队列等。
  5. 客户端 (Client):
    • 负责创建一个 ConcreteCommand 对象。
    • 设置该命令对象的接收者。
    • 将这个 Command 对象设置给 Invoker(例如,将一个“开灯命令”对象关联到遥控器的某个按钮上)。

核心角色:

  • Command (命令接口/抽象类): 声明执行操作的接口 (execute),可能还有撤销操作的接口 (undo)。
  • ConcreteCommand (具体命令): 实现 Command 接口。绑定一个接收者对象,并在 execute 方法中调用接收者的相应操作。存储执行 undo 所需的状态。
  • Receiver (接收者): 知道如何执行与请求相关的操作。
  • Invoker (调用者): 要求该命令执行这个请求。持有 Command 对象。
  • Client (客户端): 创建 ConcreteCommand 对象并设置其接收者。将 Command 对象交给 Invoker。

关键:将请求封装成包含接收者和操作信息的命令对象,调用者通过命令接口与命令对象交互,解耦调用者与接收者。

三、解耦与扩展的利器:命令模式的适用场景

命令模式在以下需要将请求对象化、解耦调用与执行、或者需要对请求进行管理的场景中非常有用:

  • 需要将请求的发起者与执行者解耦: 调用者(如按钮、菜单项)无需知道请求的具体接收者是谁以及如何执行。
  • 需要将请求参数化: 可以用不同的命令对象来配置调用者(如给同一个按钮设置不同的功能)。
  • 需要支持请求排队: 可以将命令对象放入队列中,由工作线程或其他机制按顺序执行。这在实现任务调度、线程池、异步处理时很常见 (java.lang.Runnable 就是一种命令模式的体现)。
  • 需要支持日志记录: 可以轻松地记录被执行的命令对象及其参数。
  • 需要支持事务操作: 可以将一系列命令组合成一个事务,如果中间某个命令执行失败,可以依次调用之前已执行命令的 undo() 方法进行回滚。
  • 需要支持撤销(Undo)和重做(Redo)操作: 这是命令模式最经典的应用之一。通过维护一个已执行命令的栈,可以方便地实现撤销(调用栈顶命令的 undo())和重做(将撤销的命令重新压栈并执行 execute())。文本编辑器、图形编辑器等大量使用此模式。
  • 实现宏命令 (Macro Command): 一个宏命令对象可以包含一个命令对象列表,其 execute() 方法按顺序执行列表中的所有命令。

Java 中的应用思考:

  • java.lang.Runnable: Runnable 接口只有一个 run() 方法,它封装了要在单独线程中执行的操作。Thread 类(Invoker)接收 Runnable 对象(Command),并在其 start() 方法被调用时,最终执行 Runnablerun() 方法。这完全符合命令模式的思想。
  • Swing/AWT 中的 Action 接口: Action 接口(继承自 ActionListener)不仅封装了要执行的操作 (actionPerformed 方法),还封装了与该操作相关的状态(如名称、图标、快捷键、是否启用)。按钮、菜单项(Invoker)可以关联一个 Action 对象(Command),当被触发时调用其 actionPerformed 方法。状态的改变(如禁用 Action)会自动反映到关联的 UI 组件上。
  • 数据库事务操作。

四、封装请求的实现:命令模式的 Java 实践

我们使用智能家居遥控器的例子来实现命令模式,并支持撤销功能。

1. 定义命令接口 (Command):

/*** 命令接口*/
interface Command {void execute(); // 执行操作void undo();    // 撤销操作
}

2. 创建接收者类 (Receiver):

/*** 接收者A:灯*/
class Light {String location;boolean isOn = false; // 记录灯的状态,用于撤销public Light(String location) { this.location = location; }public void on() {isOn = true;System.out.println(location + " 灯打开了");}public void off() {isOn = false;System.out.println(location + " 灯关闭了");}
}/*** 接收者B:吊扇*/
enum FanSpeed { OFF, LOW, MEDIUM, HIGH }
class CeilingFan {String location;FanSpeed speed = FanSpeed.OFF; // 记录风扇之前的速度,用于撤销public CeilingFan(String location) { this.location = location; }public void high() { speed = FanSpeed.HIGH; System.out.println(location + " 吊扇设置为高速"); }public void medium() { speed = FanSpeed.MEDIUM; System.out.println(location + " 吊扇设置为中速"); }public void low() { speed = FanSpeed.LOW; System.out.println(location + " 吊扇设置为低速"); }public void off() { speed = FanSpeed.OFF; System.out.println(location + " 吊扇关闭"); }public FanSpeed getSpeed() { return speed; }
}

3. 创建具体命令类 (ConcreteCommand):

/*** 具体命令A:开灯命令*/
class LightOnCommand implements Command {private Light light; // 持有接收者引用private boolean previousState; // 用于撤销public LightOnCommand(Light light) { this.light = light; }@Override public void execute() {previousState = light.isOn; // 记录执行前的状态light.on();}@Override public void undo() {if (previousState) { // 如果之前是开的,撤销就是开light.on();} else { // 如果之前是关的,撤销就是关light.off();}System.out.println("撤销开灯操作 for " + light.location);}
}/*** 具体命令B:关灯命令*/
class LightOffCommand implements Command {private Light light;private boolean previousState;public LightOffCommand(Light light) { this.light = light; }@Override public void execute() {previousState = light.isOn;light.off();}@Override public void undo() {if (previousState) { light.on(); } else { light.off(); }System.out.println("撤销关灯操作 for " + light.location);}
}/*** 具体命令C:吊扇高速命令*/
class CeilingFanHighCommand implements Command {private CeilingFan fan;private FanSpeed previousSpeed; // 记录之前的速度用于撤销public CeilingFanHighCommand(CeilingFan fan) { this.fan = fan; }@Override public void execute() {previousSpeed = fan.getSpeed();fan.high();}@Override public void undo() {// 撤销就是恢复到之前的速度switch (previousSpeed) {case HIGH: fan.high(); break;case MEDIUM: fan.medium(); break;case LOW: fan.low(); break;case OFF: fan.off(); break;}System.out.println("撤销吊扇高速操作 for " + fan.location + ", 恢复到 " + previousSpeed);}
}/*** 具体命令D:吊扇关闭命令*/
class CeilingFanOffCommand implements Command {private CeilingFan fan;private FanSpeed previousSpeed;public CeilingFanOffCommand(CeilingFan fan) { this.fan = fan; }@Override public void execute() {previousSpeed = fan.getSpeed();fan.off();}@Override public void undo() {switch (previousSpeed) {case HIGH: fan.high(); break;case MEDIUM: fan.medium(); break;case LOW: fan.low(); break;case OFF: fan.off(); break; // 如果之前就是关的,撤销还是关}System.out.println("撤销吊扇关闭操作 for " + fan.location + ", 恢复到 " + previousSpeed);}
}/*** 空命令 (Null Object Pattern 结合 Command): 用于未设置功能的按钮*/
class NoCommand implements Command {@Override public void execute() { System.out.println("这个按钮没有分配功能"); }@Override public void undo() { System.out.println("无操作可撤销"); }
}

4. 创建调用者类 (Invoker):

import java.util.Stack;/*** 调用者:遥控器*/
class RemoteControlWithUndo {private Command[] onCommands;  // 开按钮对应的命令private Command[] offCommands; // 关按钮对应的命令private Stack<Command> undoCommandStack = new Stack<>(); // 用于存储上一步操作的命令栈public RemoteControlWithUndo(int numberOfSlots) {onCommands = new Command[numberOfSlots];offCommands = new Command[numberOfSlots];// 初始化所有按钮为空命令Command noCommand = new NoCommand();for (int i = 0; i < numberOfSlots; i++) {onCommands[i] = noCommand;offCommands[i] = noCommand;}}// 设置按钮对应的命令public void setCommand(int slot, Command onCommand, Command offCommand) {if (slot >= 0 && slot < onCommands.length) {onCommands[slot] = onCommand;offCommands[slot] = offCommand;System.out.println("遥控器插槽 " + slot + " 设置命令: ON=" + onCommand.getClass().getSimpleName() + ", OFF=" + offCommand.getClass().getSimpleName());}}// 按下开按钮public void pressOnButton(int slot) {if (slot >= 0 && slot < onCommands.length) {System.out.println("按下开按钮,插槽 " + slot);Command command = onCommands[slot];command.execute();// 将执行的命令压入撤销栈undoCommandStack.push(command);System.out.println("操作已记录到撤销栈。");}}// 按下关按钮public void pressOffButton(int slot) {if (slot >= 0 && slot < offCommands.length) {System.out.println("按下关按钮,插槽 " + slot);Command command = offCommands[slot];command.execute();undoCommandStack.push(command);System.out.println("操作已记录到撤销栈。");}}// 按下撤销按钮public void pressUndoButton() {System.out.println("按下撤销按钮...");if (!undoCommandStack.isEmpty()) {Command lastCommand = undoCommandStack.pop(); // 从栈顶取出上一个命令System.out.println("准备撤销操作: " + lastCommand.getClass().getSimpleName());lastCommand.undo(); // 执行撤销} else {System.out.println("撤销栈为空,无操作可撤销。");}}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();sb.append("\n------ 遥控器状态 ------\n");for (int i = 0; i < onCommands.length; i++) {sb.append("[插槽 ").append(i).append("] ON: ").append(onCommands[i].getClass().getSimpleName()).append("\t OFF: ").append(offCommands[i].getClass().getSimpleName()).append("\n");}sb.append("[撤销栈大小]: ").append(undoCommandStack.size()).append("\n");sb.append("------------------------\n");return sb.toString();}
}

5. 客户端使用:

public class CommandClient {public static void main(String[] args) {// 1. 创建调用者 (遥控器)RemoteControlWithUndo remote = new RemoteControlWithUndo(3); // 假设有 3 个插槽// 2. 创建接收者 (家电)Light livingRoomLight = new Light("客厅");CeilingFan kitchenFan = new CeilingFan("厨房");Light bedRoomLight = new Light("卧室");// 3. 创建具体命令对象,并设置接收者Command livingRoomLightOn = new LightOnCommand(livingRoomLight);Command livingRoomLightOff = new LightOffCommand(livingRoomLight);Command kitchenFanHigh = new CeilingFanHighCommand(kitchenFan);Command kitchenFanOff = new CeilingFanOffCommand(kitchenFan);Command bedRoomLightOn = new LightOnCommand(bedRoomLight);Command bedRoomLightOff = new LightOffCommand(bedRoomLight);// 4. 将命令设置到遥控器的插槽上remote.setCommand(0, livingRoomLightOn, livingRoomLightOff);remote.setCommand(1, kitchenFanHigh, kitchenFanOff);remote.setCommand(2, bedRoomLightOn, bedRoomLightOff);// 打印遥控器初始状态System.out.println(remote);// 5. 模拟按下按钮System.out.println("=== 模拟操作 ===");remote.pressOnButton(0);   // 开客厅灯remote.pressOffButton(0);  // 关客厅灯remote.pressOnButton(1);   // 开厨房风扇高速remote.pressUndoButton(); // 撤销开厨房风扇高速 (风扇应恢复之前状态,即关闭)remote.pressOnButton(2);   // 开卧室灯remote.pressOffButton(1);  // 关闭厨房风扇 (它已经是关的,再关一次)remote.pressUndoButton(); // 撤销关闭厨房风扇 (应恢复到关闭状态)remote.pressUndoButton(); // 撤销开卧室灯 (卧室灯应关闭)remote.pressUndoButton(); // 撤销关客厅灯 (客厅灯应打开)System.out.println(remote); // 打印最终状态和撤销栈}
}

代码解读:

  • Command 接口定义了 executeundo
  • Light, CeilingFan 是接收者,包含执行具体操作的方法和必要的状态(用于撤销)。
  • LightOnCommand, LightOffCommand, CeilingFanHighCommand, CeilingFanOffCommand 是具体命令,它们持有接收者引用,在 execute 中调用接收者方法,并在 undo 中实现反向操作(通过记录之前的状态)。NoCommand 是空对象模式的应用。
  • RemoteControlWithUndo 是调用者,它不直接与家电交互,而是持有 Command 对象数组。按下按钮时执行对应命令的 execute,并将命令压入 undoCommandStack。按下撤销按钮时,从栈顶弹出命令并执行其 undo
  • 客户端负责创建命令、设置接收者,并将命令关联到调用者(遥控器按钮)。

五、模式的价值:命令模式带来的解耦与灵活性

命令模式的核心价值在于其强大的解耦能力对请求的管理能力

  1. 解耦调用者与接收者 (Decouples Invoker and Receiver): 调用者(如遥控器)完全不需要知道请求的接收者(如灯、风扇)是谁,以及如何执行。调用者只与抽象的 Command 接口交互。这极大地降低了它们之间的耦合度。
  2. 请求对象化 (Request as Object): 将请求封装成对象,使得请求可以像其他对象一样被传递、存储、参数化。
  3. 易于添加新命令 (Easy to Add New Commands): 增加新的操作只需要创建一个新的 ConcreteCommand 类,符合开闭原则 (OCP)。无需修改调用者或接收者的代码。
  4. 支持高级功能 (Supports Advanced Features):
    • 撤销/重做: 实现 undo() 方法并维护命令历史即可轻松实现。
    • 队列/日志: 命令对象可以被放入队列异步执行,或者序列化后存入日志。
    • 宏命令: 可以创建一个组合命令(CompositeCommand)来执行一系列子命令。
    • 事务: 可以将一组命令视为一个事务单元,统一执行和回滚。

六、权衡与考量:命令模式的类膨胀问题

引入命令模式也可能带来一些影响:

  • 可能导致类的数量增加 (Increased Number of Classes): 每个具体的操作都需要一个对应的 ConcreteCommand 类。如果系统中有大量的操作,可能会导致类的数量显著增加。(同样,Java 8+ 的 Lambda 表达式可以在一定程度上简化只有一个 execute 方法的命令实现)。
  • 实现撤销的复杂性 (Complexity of Undo): 实现 undo() 方法可能比较复杂,需要仔细管理状态的保存与恢复。如果操作不可逆,或者状态恢复成本很高,实现撤销可能会很困难或不切实际。

七、心法归纳:封装请求,解耦执行

命令模式的核心“心法”在于**“封装请求”与“解耦执行”**:

  1. 封装请求 (Encapsulate Request): 将一个操作请求(包括所需的接收者信息和执行逻辑)打包成一个独立的命令对象
  2. 解耦执行 (Decouple Invoker and Receiver): 调用者(Invoker)不再直接调用接收者(Receiver)的方法,而是通过命令对象execute() 方法来间接触发操作。调用者与接收者之间通过命令对象实现了解耦。

掌握命令模式,意味着你拥有了:

  • 将操作本身变成可管理实体的能力: 可以对请求进行排队、记录、撤销等。
  • 实现调用者与执行者松耦合的强大武器。
  • 构建可扩展、可撤销、支持事务等高级功能系统的基础。
  • 理解 Runnable、GUI Action 等常见 Java API 设计思想的钥匙。

当你需要将请求的发起和执行分离,或者需要对请求本身进行各种管理(如撤销、排队、记录)时,命令模式就是你设计工具箱中那把能够“化请求为对象”、带来无限可能的“魔法棒”。它体现了面向对象中封装、抽象和解耦的精髓,是构建灵活、健壮交互系统的关键模式。


下一章预告: 《Java 设计模式心法:备忘录 (Memento) - 捕获与恢复对象状态的“时光机”》。如果我们想在不破坏对象封装性的前提下,保存一个对象在某个时刻的内部状态,以便将来能够恢复到这个状态(例如实现撤销或快照功能),该怎么办?备忘录模式将为我们展示如何优雅地实现对象的“状态存档”与“时光倒流”。敬请期待!

http://www.xdnf.cn/news/2105.html

相关文章:

  • verilog中实现单周期cpu的RVM指令(乘除取模)
  • 登高架设作业证考试的实操项目有哪些?
  • 前端八股 2
  • 支持私有化部署的电子合同平台——一合通
  • 01.oracle SQL基础
  • 使用Go语言实现轻量级消息队列
  • Ubuntu系统卡机日志笔记
  • OpenHarmony 5.0设置锁屏密码失败
  • QuecPython+USBNET:实现USB网卡功能
  • 真.从“零”搞 VSCode+STM32CubeMx+C <2>调试+烧录
  • docker-compose安装RustDesk远程工具
  • 工业电子测量中的安全隐患与解决方案——差分探头的技术优势解析
  • 如何在SpringBoot中通过@Value注入Map和List并使用YAML配置?
  • 分账解决连锁酒店资金分配难题
  • Langchain文本摘要
  • Exposure Adjusted Incidence Rate (EAIR) 暴露调整发病率:精准量化疾病风险
  • 基于Python或Java实现的本地知识库文档问答系统
  • 解锁大数据新视野:构建强大可观测平台
  • Scala语法基础
  • window和ubuntu自签证书
  • SD3302 驱动:轻量级模块化,一键集成,高效易用。
  • PTC加热片详解(STM32)
  • kvm物理接口发现的脚本COLT_CMDB_KVM_IFACE.sh
  • Qt指ModbusTcp协议的使用
  • 潇洒郎:ssh 连接Windows WSL2 Linux子系统 ipv6地址转发到ipv4地址上
  • BTSRB德国交通标志数据集.csv文件提取数据转换成.json文件
  • UVM 寄存器模型中的概念
  • 国标GB28181视频平台EasyGBS视频监控平台助力打造校园安防智能化
  • 剖析经典二维动画的制作流程,汲取经验
  • SpringBoot集成LiteFlow实现轻量级工作流引擎