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

发布于:2025-06-10 ⋅ 阅读:(20) ⋅ 点赞:(0)

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

import "fmt"

// Light 是接收者
type Light struct {
	Location string
	isOn bool
}

func NewLight(location string) *Light {
	return &Light{Location: location}
}

func (l *Light) On() {
	l.isOn = true
	fmt.Printf("%s light is ON\n", l.Location)
}

func (l *Light) Off() {
	l.isOn = false
	fmt.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 commands

import "../devices"

// LightOnCommand 是一个具体命令
type LightOnCommand struct {
	Light *devices.Light
	previousState 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 commands

import "../devices"

// LightOffCommand 是一个具体命令
type LightOffCommand struct {
	Light *devices.Light
	previousState 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; // 用于 undo

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        previousState = light.isOn; // 保存执行前的状态
        light.on();
    }

    @Override
    public 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; // 用于 undo

    public LightOffCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        previousState = light.isOn; // 保存执行前的状态
        light.off();
    }

    @Override
    public void undo() {
        if (previousState) { // 如果之前是开着的,就恢复开
            light.on();
        } else { // 如果之前是关着的,就恢复关
            light.off();
        }
    }
}
调用者 (SimpleRemoteControl - Invoker)
// simple_remote_control.go (Invoker)
package invoker

import "../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; // 用于 undo

    public 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 main

import (
	"./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 ON

	fmt.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 是 false

	fmt.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 ON
	remote.SetCommand(lightOff) // Set command to LightOff
	remote.ButtonWasPressed()     // Execute LightOff: Living Room light is OFF
	// Now, undoing LightOff should turn it back ON
	fmt.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 nil
	noCommandRemote.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 ON

        System.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 = kitchenLightOn

        remote.setCommand(kitchenLightOff);
        remote.buttonWasPressed(); // Kitchen light is OFF. lastCommand = kitchenLightOff

        System.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 demonstration
        System.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. 总结

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