第四部分:行為型模式 - 命令模式 (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)
命令模式的主要目的:
- 將請求的發送者和接收者解耦:發送者(Invoker)只需要知道如何發出命令,而不需要知道命令的具體接收者是誰,以及接收者是如何執行操作的。
- 將請求封裝成對象:這使得請求可以像其他對象一樣被傳遞、存儲、排隊、記錄日志等。
- 支持參數化方法調用:可以將命令對象作為參數傳遞給方法。
- 支持撤銷和重做操作:通過保存已執行命令的歷史記錄,可以實現撤銷(undo)和重做(redo)功能。
- 支持事務性操作:可以將一系列命令組合成一個宏命令(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)
命令模式通常包含以下角色:
- Command (命令接口/抽象類):聲明了一個執行操作的接口,通常只有一個方法,如
execute()
。有時也會包含undo()
方法。 - ConcreteCommand (具體命令):實現 Command 接口。它持有一個接收者(Receiver)對象的引用,并調用接收者的方法來完成具體的請求。它將一個接收者對象與一個動作綁定起來。
- Receiver (接收者):知道如何實施與執行一個請求相關的操作。任何類都可能作為一個接收者。
- Invoker (調用者/請求者):持有一個命令對象,并要求該命令執行請求。調用者不直接訪問接收者,而是通過命令對象間接調用。
- Client (客戶端):創建具體命令對象,并設置其接收者。然后將命令對象配置給調用者。
工作流程:
- 客戶端創建一個或多個具體命令對象,并為每個命令對象設置其接收者。
- 客戶端將這些命令對象配置給一個或多個調用者對象。
- 當某個事件發生時(例如用戶點擊按鈕),調用者調用其命令對象的
execute()
方法。 - 具體命令對象的
execute()
方法會調用其關聯的接收者對象的相應方法來執行實際操作。 - 如果支持撤銷,
undo()
方法會執行與execute()
相反的操作。
4. 適用場景 (When to Use)
- 當你想參數化對象以及它們所執行的操作時(例如,GUI按鈕的行為)。
- 當你想將請求排隊、記錄請求日志或支持可撤銷的操作時。
- 當你想將操作的請求者與操作的執行者解耦時。
- 當你想用對象來表示操作,并且這些操作可以被存儲、傳遞和調用時。
- 實現回調機制:命令對象可以看作是回調函數的面向對象替代品。
- 實現宏命令:一個宏命令是多個命令的組合,可以像單個命令一樣執行。
5. 優缺點 (Pros and Cons)
優點:
- 降低耦合度:調用者和接收者之間解耦。調用者不需要知道接收者的任何細節。
- 易于擴展:增加新的命令非常容易,只需創建新的 ConcreteCommand 類,符合開閉原則。
- 支持組合命令(宏命令):可以將多個命令組合成一個復合命令。
- 方便實現 Undo/Redo:命令對象可以保存執行操作所需的狀態,從而支持撤銷和重做。
- 方便實現請求的排隊和日志記錄:由于請求被封裝成對象,可以很容易地將它們存儲起來。
缺點:
- 可能導致系統中產生大量具體命令類:如果有很多不同的操作,可能會導致類的數量膨脹。
- 每個具體命令都需要實現執行邏輯,可能會有重復代碼(如果操作類似但接收者不同)。
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. 總結
命令模式是一種強大的行為設計模式,它通過將請求封裝成對象,實現了請求發送者和接收者之間的解耦。這不僅使得系統更加靈活和可擴展,還為實現諸如操作的排隊、日志記錄、撤銷/重做以及宏命令等高級功能提供了基礎。當你需要將“做什么”(請求)與“誰做”(接收者)以及“何時/如何做”(調用者)分離時,命令模式是一個非常值得考慮的選擇。