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