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

第四部分:行为型模式 - 命令模式 (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. 总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/bicheng/84568.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

禁止 Windows 更新后自动重启

Windows 默认会在安装重要更新后自动重启,但你可以调整设置来避免这种情况: ​​方法 1:通过组策略(适用于 Windows 专业版/企业版)​​ 按 Win R,输入 gpedit.msc 打开 ​​本地组策略编辑器​​。导航…

GoldenDB简述

GoldenDB是国产的分布式数据库。它彻底解决了事务一致性,数据实时一致性的问题。采用的是Shared Nothing(分片式存储)的分布式架构。就是不共享数据,各自节点持有各自的数据。对比不共享的,还有其他两种分布式架构&…

训练过程中的 Loss ?

文章目录 在我们训练的过程中,设置好这个epochs也就是训练的轮次,然后计算这个损失函数,我们可以知道这个具体的训练的情况,那么在训练的过程中,这个损失函数的变化有哪些情况?对应的一个解释情况是怎么样的…

S2B2B农产品供应链交易多平台开发有哪些发展前景?如何维护?

一、S2B2B农产品供应链交易多平台开发的未来发展前景 本文将由小编为您介绍关于S2B2B农产品供应链交易多平台开发的内容,希望能够帮助大家。在数字化时代,农产品供应链的数字化转型成为了一种必然趋势。S2B2B(Supplier to Business to Business)模式通过…

关于有害的过度使用 std::move

翻译:2023 11 月 24 日On harmful overuse of std::move cppreference std::move 论 std::move 的有害过度使用 - The Old New Thing C 的 std::move 函数将其参数转换为右值引用,这使得其内容可以被另一个操作“消费”(移动)。…

Ubuntu24.04 onnx 模型转 rknn

前面的环境配置有点懒得写,教程也很多,可以自己找 rknn-toolkit2 gitee 地址:pingli/rknn-toolkit2 试了很多开源的代码,都没办法跑通, 最后自己改了一版 微调后的 qwen2 模型适用 from rknn.api import RKNN impor…

Electron通信流程

前言 今天讲Electron框架的通信流程,首先我们需要知道为什么需要通信。这得益于Electron的多进程模型,它主要模仿chrome的多进程模型如下图: 作为应用开发者,我们将控制两种类型的进程:主进程和渲染器进程 。 …

uni-app项目实战笔记1--创建项目和实现首页轮播图功能

ps:本笔记来自B站咸虾米壁纸项目 一.创建项目,完成项目初始化搭建 1.在HBuilder X创建wallper项目,使用默认模块,选择vue; 2.在项目根目录下创建common目录,用于存放静态资源,创建项目时自动生成static目…

机械制造系统中 PROFINET 与 PROFIBUS-DP 的融合应用及捷米科技解决方案

在机械制造领域,工业通信网络的兼容性与灵活性直接影响产线的自动化水平与生产效率。当前,多数机械制造系统采用PROFINET 控制器构建核心网络架构,并通过微波无线连接实现设备互联。随着工业网络的发展,系统中常需同时集成PROFINE…

MCP 协议系列序言篇:开启 AI 应用融合新时代的钥匙

文章目录 序言:AI 应用层进入 MCP 时代为什么 MCP 开启 AI 应用融合新时代的钥匙为什么是 MCP?它与 Function Calling、Agent 有什么区别?Function CallingAI AgentMCP(Model Context Protocol) MCP 如何工作MCP Serve…

【threejs】每天一个小案例讲解:光照

代码仓 GitHub - TiffanyHoo/three_practices: Learning three.js together! 可自行clone,无需安装依赖,直接liver-server运行/直接打开chapter01中的html文件 运行效果图 知识要点 常见光照类型及其特点如下: 1. 环境光(Ambi…

大模型在输尿管下段积水预测及临床应用的研究

目录 一、引言 1.1 研究背景与意义 1.2 研究目的 1.3 研究范围与限制 1.4 文献综述 1.5 研究方法和框架 二、相关理论与概念 2.1 大模型技术原理 2.2 输尿管下段积水病理机制 2.3 大模型在医学预测领域的应用 三、大模型预测输尿管下段积水的方法 3.1 数据收集 3.…

gitlab相关操作

2025.06.11今天我学习了如何在终端使用git相关操作: 一、需要修改新的仓库git地址的时候: (1)检查当前远程仓库 git remote -v 输出示例: origin https://github.com/old-repo.git (fetch) origin https://github.c…

51c自动驾驶~合集58

我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…

通过共享内存在多程序之间实现数据通信

注:以下内容为与 GPT-4O 共同创作完成 以共享内存的方式实现多程序之间的数据通信,尤其适合在一台机器上的多程序之间进行高频数据交换。 以下示例展示了 sender.py 向 receiver.py 发送数据并接收经 receiver.py 处理后的数据,以及如何通过…

[论文阅读] 人工智能+软件工程 | 理解GitGoodBench:评估AI代理在Git中表现的新基准

理解GitGoodBench:评估AI代理在Git中表现的新基准 论文信息 GitGoodBench: A Novel Benchmark For Evaluating Agentic Performance On Git Tobias Lindenbauer, Egor Bogomolov, Yaroslav Zharov Cite as: arXiv:2505.22583 [cs.SE] 研究背景:当AI走进…

开源 java android app 开发(十二)封库.aar

文章的目的为了记录使用java 进行android app 开发学习的经历。本职为嵌入式软件开发,公司安排开发app,临时学习,完成app的开发。开发流程和要点有些记忆模糊,赶紧记录,防止忘记。 相关链接: 开源 java an…

ubuntu + nginx 1.26 + php7.4 + mysql8.0 调优

服务器配置 8核 16G 查看内存 free -h nginx配置 worker_processes auto; # 自动检测CPU核心数 worker_rlimit_nofile 65535; # 提高文件描述符限制 ​ events {worker_connections 8192; # 每个worker的最大连接数multi_accept on; # 一次性接受…

[未验证]abaqus2022 更改内置python

如何在 Abaqus 2022 中更改内置 Python 在 Abaqus 中,Python 是常用的脚本语言,它使得用户能够自动化模型的创建、分析和后处理。可能有时候你需要更改默认的 Python 版本,比如使用特定库或者功能。本文将为您详细说明如何在 Abaqus 2022 中更…

RAG文档解析难点2:excel数据“大海捞针”,超大Excel解析与精准行列查询指南

写在前面 在构建检索增强生成(RAG)应用时,Excel文件是不可或缺的数据源。它们通常包含了企业运营、市场分析、科学研究等各个领域的宝贵数据。然而,当这些Excel文件变得“超大”——可能包含数十万甚至数百万行数据时,传统的解析方法和RAG数据处理流程将面临严峻的内存、…