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

零基础设计模式——行为型模式 - 命令模式

第四部分:行为型模式 - 命令模式 (Command Pattern)

接下来,我们学习行为型模式中的命令模式。这个模式能将“请求”封装成一个对象,从而让你能够参数化客户端对象,将请求排队或记录请求日志,以及支持可撤销的操作。

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

命令模式 (Command Pattern)

“将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。” (Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.)

想象一下你去餐厅点餐:

  • 你 (Client):想点一份宫保鸡丁。
  • 服务员 (Invoker):记录下你的点单(“宫保鸡丁一份”),这个点单就像一个“命令对象”。服务员并不自己做菜。
  • 点菜单/小票 (Command Object):上面写着“宫保鸡丁”,它封装了你的请求。
  • 厨师 (Receiver):拿到点菜单后,知道要做什么菜(执行命令),然后开始烹饪宫保鸡丁。

在这个过程中:

  • 你不需要知道厨师是谁,厨师也不需要直接和你交流。
  • 服务员(调用者)和厨师(接收者)解耦了。
  • 点菜单(命令对象)可以在服务员和厨师之间传递,甚至可以排队(如果厨师很忙)。如果点错了,理论上也可以撤销这个点单(如果还没开始做)。

1. 目的 (Intent)

命令模式的主要目的:

  1. 将请求的发送者和接收者解耦:发送者(Invoker)只需要知道如何发出命令,而不需要知道命令的具体接收者是谁,以及接收者是如何执行操作的。
  2. 将请求封装成对象:这使得请求可以像其他对象一样被传递、存储、排队、记录日志等。
  3. 支持参数化方法调用:可以将命令对象作为参数传递给方法。
  4. 支持撤销和重做操作:通过保存已执行命令的历史记录,可以实现撤销(undo)和重做(redo)功能。
  5. 支持事务性操作:可以将一系列命令组合成一个宏命令(Macro Command),要么全部执行,要么全部不执行。

2. 生活中的例子 (Real-world Analogy)

  • 电视遥控器

    • 你 (Client):按下遥控器上的“开机”按钮。
    • 遥控器 (Invoker):发送一个“开机”信号。
    • “开机”信号 (Command Object):封装了开启动作的请求。
    • 电视机 (Receiver):接收到信号并执行开机操作。
      每个按钮(音量+、换台等)都对应一个命令对象。
  • 电灯开关

    • 开关 (Invoker):你按动开关。
    • “开灯”或“关灯”的动作 (Command Object):被封装。
    • 电灯 (Receiver):执行开关灯的动作。
  • GUI按钮和菜单项

    • 点击一个按钮或菜单项(如“保存文件”)。
    • 按钮/菜单项 (Invoker) 触发一个命令对象。
    • 命令对象知道如何调用应用程序的某个模块 (Receiver) 来执行保存操作。
  • 任务队列 (Task Queues)

    • 系统将待处理的任务(如发送邮件、处理图片)封装成命令对象,放入队列中。
    • 工作线程 (Worker Threads - Invokers/Receivers) 从队列中取出命令并执行。

3. 结构 (Structure)

命令模式通常包含以下角色:

  1. Command (命令接口/抽象类):声明了一个执行操作的接口,通常只有一个方法,如 execute()。有时也会包含 undo() 方法。
  2. ConcreteCommand (具体命令):实现 Command 接口。它持有一个接收者(Receiver)对象的引用,并调用接收者的方法来完成具体的请求。它将一个接收者对象与一个动作绑定起来。
  3. Receiver (接收者):知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
  4. Invoker (调用者/请求者):持有一个命令对象,并要求该命令执行请求。调用者不直接访问接收者,而是通过命令对象间接调用。
  5. Client (客户端):创建具体命令对象,并设置其接收者。然后将命令对象配置给调用者。
    在这里插入图片描述
    工作流程
  • 客户端创建一个或多个具体命令对象,并为每个命令对象设置其接收者。
  • 客户端将这些命令对象配置给一个或多个调用者对象。
  • 当某个事件发生时(例如用户点击按钮),调用者调用其命令对象的 execute() 方法。
  • 具体命令对象的 execute() 方法会调用其关联的接收者对象的相应方法来执行实际操作。
  • 如果支持撤销,undo() 方法会执行与 execute() 相反的操作。

4. 适用场景 (When to Use)

  • 当你想参数化对象以及它们所执行的操作时(例如,GUI按钮的行为)。
  • 当你想将请求排队、记录请求日志或支持可撤销的操作时。
  • 当你想将操作的请求者与操作的执行者解耦时。
  • 当你想用对象来表示操作,并且这些操作可以被存储、传递和调用时。
  • 实现回调机制:命令对象可以看作是回调函数的面向对象替代品。
  • 实现宏命令:一个宏命令是多个命令的组合,可以像单个命令一样执行。

5. 优缺点 (Pros and Cons)

优点:

  1. 降低耦合度:调用者和接收者之间解耦。调用者不需要知道接收者的任何细节。
  2. 易于扩展:增加新的命令非常容易,只需创建新的 ConcreteCommand 类,符合开闭原则。
  3. 支持组合命令(宏命令):可以将多个命令组合成一个复合命令。
  4. 方便实现 Undo/Redo:命令对象可以保存执行操作所需的状态,从而支持撤销和重做。
  5. 方便实现请求的排队和日志记录:由于请求被封装成对象,可以很容易地将它们存储起来。

缺点:

  1. 可能导致系统中产生大量具体命令类:如果有很多不同的操作,可能会导致类的数量膨胀。
  2. 每个具体命令都需要实现执行逻辑,可能会有重复代码(如果操作类似但接收者不同)。

6. 实现方式 (Implementations)

让我们以一个简单的遥控器控制电灯的例子来说明。

接收者 (Light - Receiver)
// light.go (Receiver)
package devicesimport "fmt"// Light 是接收者
type Light struct {Location stringisOn bool
}func NewLight(location string) *Light {return &Light{Location: location}
}func (l *Light) On() {l.isOn = truefmt.Printf("%s light is ON\n", l.Location)
}func (l *Light) Off() {l.isOn = falsefmt.Printf("%s light is OFF\n", l.Location)
}
// Light.java (Receiver)
package com.example.devices;public class Light {String location;boolean isOn;public Light(String location) {this.location = location;}public void on() {isOn = true;System.out.println(location + " light is ON");}public void off() {isOn = false;System.out.println(location + " light is OFF");}
}
命令接口 (Command)
// command.go (Command interface)
package commands// Command 接口
type Command interface {Execute()Undo() // 添加 Undo 方法
}
// Command.java (Command interface)
package com.example.commands;public interface Command {void execute();void undo(); // 添加 Undo 方法
}
具体命令 (LightOnCommand, LightOffCommand - ConcreteCommand)
// light_on_command.go
package commandsimport "../devices"// LightOnCommand 是一个具体命令
type LightOnCommand struct {Light *devices.LightpreviousState bool // 用于 undo
}func NewLightOnCommand(light *devices.Light) *LightOnCommand {return &LightOnCommand{Light: light}
}func (c *LightOnCommand) Execute() {c.previousState = c.Light.IsOn // 保存执行前的状态c.Light.On()
}func (c *LightOnCommand) Undo() {if c.previousState { // 如果之前是开着的,就恢复开c.Light.On()} else { // 如果之前是关着的,就恢复关c.Light.Off()}
}// light_off_command.go
package commandsimport "../devices"// LightOffCommand 是一个具体命令
type LightOffCommand struct {Light *devices.LightpreviousState bool // 用于 undo
}func NewLightOffCommand(light *devices.Light) *LightOffCommand {return &LightOffCommand{Light: light}
}func (c *LightOffCommand) Execute() {c.previousState = c.Light.IsOn // 保存执行前的状态c.Light.Off()
}func (c *LightOffCommand) Undo() {if c.previousState { // 如果之前是开着的,就恢复开c.Light.On()} else { // 如果之前是关着的,就恢复关c.Light.Off()}
}
// LightOnCommand.java (ConcreteCommand)
package com.example.commands;import com.example.devices.Light;public class LightOnCommand implements Command {Light light;boolean previousState; // 用于 undopublic LightOnCommand(Light light) {this.light = light;}@Overridepublic void execute() {previousState = light.isOn; // 保存执行前的状态light.on();}@Overridepublic void undo() {if (previousState) { // 如果之前是开着的,就恢复开light.on();} else { // 如果之前是关着的,就恢复关light.off();}}
}// LightOffCommand.java (ConcreteCommand)
package com.example.commands;import com.example.devices.Light;public class LightOffCommand implements Command {Light light;boolean previousState; // 用于 undopublic LightOffCommand(Light light) {this.light = light;}@Overridepublic void execute() {previousState = light.isOn; // 保存执行前的状态light.off();}@Overridepublic void undo() {if (previousState) { // 如果之前是开着的,就恢复开light.on();} else { // 如果之前是关着的,就恢复关light.off();}}
}
调用者 (SimpleRemoteControl - Invoker)
// simple_remote_control.go (Invoker)
package invokerimport "../commands"// SimpleRemoteControl 是一个简单的调用者
type SimpleRemoteControl struct {slot commands.Command // 持有一个命令对象
}func NewSimpleRemoteControl() *SimpleRemoteControl {return &SimpleRemoteControl{}
}func (r *SimpleRemoteControl) SetCommand(command commands.Command) {r.slot = command
}func (r *SimpleRemoteControl) ButtonWasPressed() {if r.slot != nil {r.slot.Execute()}
}func (r *SimpleRemoteControl) UndoButtonWasPressed() {if r.slot != nil {r.slot.Undo()}
}
// SimpleRemoteControl.java (Invoker)
package com.example.invoker;import com.example.commands.Command;public class SimpleRemoteControl {Command slot; // 持有一个命令对象Command lastCommand; // 用于 undopublic SimpleRemoteControl() {}public void setCommand(Command command) {this.slot = command;}public void buttonWasPressed() {if (slot != null) {slot.execute();lastCommand = slot; // 保存最后执行的命令}}public void undoButtonWasPressed() {if (lastCommand != null) {System.out.print("Undoing: ");lastCommand.undo();lastCommand = null; // 一次撤销后清除,或者使用命令栈}}
}
客户端使用
// main.go (示例用法)
/*
package mainimport ("./commands""./devices""./invoker""fmt"
)func main() {remote := invoker.NewSimpleRemoteControl()// 创建接收者livingRoomLight := devices.NewLight("Living Room")// 创建命令并关联接收者lightOn := commands.NewLightOnCommand(livingRoomLight)lightOff := commands.NewLightOffCommand(livingRoomLight)// --- 测试开灯 ---fmt.Println("--- Testing Light ON ---")remote.SetCommand(lightOn)remote.ButtonWasPressed() // Living Room light is ONfmt.Println("--- Testing Undo for Light ON (should turn OFF) ---")remote.UndoButtonWasPressed() // Living Room light is OFF (assuming it was off before 'on')// --- 测试关灯 ---fmt.Println("\n--- Testing Light OFF ---")remote.SetCommand(lightOff)remote.ButtonWasPressed() // Living Room light is OFF// 此时 livingRoomLight.IsOn 是 falsefmt.Println("--- Testing Undo for Light OFF (should turn ON if it was ON before 'off') ---")// 为了让undo有意义,我们先打开灯,再执行关灯命令,再撤销关灯命令fmt.Println("\n--- Setting up for Undo OFF test ---")livingRoomLight.On() // Manually turn light on: Living Room light is ONremote.SetCommand(lightOff) // Set command to LightOffremote.ButtonWasPressed()     // Execute LightOff: Living Room light is OFF// Now, undoing LightOff should turn it back ONfmt.Println("--- Undoing Light OFF ---")remote.UndoButtonWasPressed() // Living Room light is ON// --- 测试没有命令时按按钮 ---fmt.Println("\n--- Testing No Command ---")noCommandRemote := invoker.NewSimpleRemoteControl()noCommandRemote.ButtonWasPressed() // No output, as slot is nilnoCommandRemote.UndoButtonWasPressed() // No output
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.commands.Command;
import com.example.commands.LightOnCommand;
import com.example.commands.LightOffCommand;
import com.example.devices.Light;
import com.example.invoker.SimpleRemoteControl;public class Main {public static void main(String[] args) {SimpleRemoteControl remote = new SimpleRemoteControl();// 创建接收者Light livingRoomLight = new Light("Living Room");// 创建命令并关联接收者Command lightOn = new LightOnCommand(livingRoomLight);Command lightOff = new LightOffCommand(livingRoomLight);// --- 测试开灯 ---System.out.println("--- Testing Light ON ---");remote.setCommand(lightOn);remote.buttonWasPressed(); // Living Room light is ONSystem.out.println("--- Testing Undo for Light ON (should turn OFF) ---");remote.undoButtonWasPressed(); // Undoing: Living Room light is OFF// --- 测试关灯 ---System.out.println("\n--- Testing Light OFF ---");remote.setCommand(lightOff);remote.buttonWasPressed(); // Living Room light is OFF// At this point, livingRoomLight.isOn is false.// The previousState in lightOff command is true (because it was on before off was executed).System.out.println("--- Testing Undo for Light OFF (should turn ON) ---");remote.undoButtonWasPressed(); // Undoing: Living Room light is ON// --- 测试更复杂的场景:先开,再关,再撤销关,再撤销开 ---System.out.println("\n--- Complex Undo Scenario ---");Light kitchenLight = new Light("Kitchen");Command kitchenLightOn = new LightOnCommand(kitchenLight);Command kitchenLightOff = new LightOffCommand(kitchenLight);remote.setCommand(kitchenLightOn);remote.buttonWasPressed(); // Kitchen light is ON. lastCommand = kitchenLightOnremote.setCommand(kitchenLightOff);remote.buttonWasPressed(); // Kitchen light is OFF. lastCommand = kitchenLightOffSystem.out.println("Undo last action (Light OFF for Kitchen):");remote.undoButtonWasPressed(); // Undoing: Kitchen light is ON. (kitchenLightOff.undo() called)// lastCommand is now null in this simple remote.// For a stack-based undo, we'd pop kitchenLightOff and kitchenLightOn would be next.// To demonstrate undoing the 'ON' command, we'd need a history stack for commands.// Our current SimpleRemoteControl only remembers the very last command for undo.// Let's simulate setting the 'ON' command again and then undoing it.System.out.println("Simulating undo for the initial ON command (requires command history):");// If we had a history stack, and popped LightOff, LightOn would be next.// Let's assume we 're-pushed' LightOn to the 'lastCommand' slot for this example.remote.lastCommand = kitchenLightOn; // Manually setting for demonstrationSystem.out.println("Undo action before last (Light ON for Kitchen):");remote.undoButtonWasPressed(); // Undoing: Kitchen light is OFF.}
}
*/

关于 Undo/Redo 的进一步说明

  • 在上面的简单遥控器 SimpleRemoteControl (Java版) 中,undoButtonWasPressed() 仅能撤销最后一次执行的命令。更完善的撤销/重做系统通常会使用一个命令历史栈(Command History Stack)。
  • 当一个命令被执行时,它被压入撤销栈。
  • 执行撤销操作时,从撤销栈中弹出一个命令,调用其 undo() 方法,然后该命令可以被压入重做栈。
  • 执行重做操作时,从重做栈中弹出一个命令,调用其 execute() 方法,然后该命令被压回撤销栈。
  • Go的示例中,UndoButtonWasPressed 撤销的是当前 slot 里的命令,这更像是一个按钮对应一个操作及其撤销,而不是全局的最后操作撤销。要实现类似Java的最后操作撤销,Go的 Invoker 也需要记录 lastCommand

7. 总结

命令模式是一种强大的行为设计模式,它通过将请求封装成对象,实现了请求发送者和接收者之间的解耦。这不仅使得系统更加灵活和可扩展,还为实现诸如操作的排队、日志记录、撤销/重做以及宏命令等高级功能提供了基础。当你需要将“做什么”(请求)与“谁做”(接收者)以及“何时/如何做”(调用者)分离时,命令模式是一个非常值得考虑的选择。

相关文章:

  • Unity | AmplifyShaderEditor插件基础(第六集:平面波动shader)
  • java中word快速转pdf
  • 2.2.2 ASPICE的需求分析
  • 大话软工笔记—需求分析概述
  • Vue 3 + WebSocket 实战:公司通知实时推送功能详解
  • 测试微信模版消息推送
  • Yii2项目自动向GitLab上报Bug
  • Linux-08 ubuntu 的 chrome浏览器不能使用 搜狗 输入法,但是火狐可以
  • 【MATLAB第119期】基于MATLAB的KRR多输入多输出全局敏感性分析模型运用(无目标函数,考虑代理模型)
  • 原型模式深度解析:Java设计模式实战指南与克隆机制优化实践
  • C# dll版本冲突解决方案
  • C# 使用表达式树(Expression Tree)代替反射赋值
  • 机器学习之聚类Kmeans算法
  • MongoDB 入门指南:安装、配置与 Navicat 连接教程
  • 冒泡排序C++实现
  • Linux系统部署KES
  • MySQL中text,longtext,mediumtext区别
  • CSS设置元素的宽度根据其内容自动调整
  • 如何将数据从 iPhone 传输到 Android?
  • 网页版便签应用开发:HTML5本地存储与拖拽交互实践
  • 网站被k多久恢复/北京中文seo
  • 用brackets做网站/深圳知名seo公司
  • 微信开发商是谁/搜seo
  • 网站开发实操记录/软文营销范文
  • 秦皇岛网站制作与网站建设/百度指数如何提升
  • 网络营销导向的网站建设的基本原则/b站视频推广的方法有哪些