零基礎設計模式——行為型模式 - 命令模式

第四部分:行為型模式 - 命令模式 (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
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/84568.shtml
英文地址,請注明出處:http://en.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數據處理流程將面臨嚴峻的內存、…