python设计模式–命令模式(Command Pattern)

Python 设计模式——命令模式(Command Pattern)详解

本文面向零基础新手,从概念到“请求对象化”、从简单示例到撤销/重做与宏命令,全方位讲解命令模式。


一、什么是命令模式?

1.1 通俗理解

命令模式是一种行为型设计模式,核心思想是:

把“请求”(要做的一件事)封装成一个对象(命令对象);这样请求就可以像普通对象一样被传递、存储、排队、记录,也可以支持撤销/重做。调用方(触发者)只负责“发出命令”,不关心命令具体由谁执行、怎么执行;执行逻辑在命令对象里,命令对象再去调用真正的“干活者”(接收者)。

可以这样类比:

  • 现实中的命令:遥控器上有一个“开灯”按钮。你按下去时,遥控器并不知道灯在哪、怎么开,它只是“发出一个开灯命令”。这个命令被封装成一个小对象,传到灯那里,由灯执行“开”。若把命令记下来,就可以实现“撤销”(再发一个关灯命令)或“重做”;也可以把多个命令排成队,按顺序执行(宏)。
  • 代码里:点击“保存”时,不直接调“保存逻辑”,而是创建一个“保存命令”对象,交给“命令执行者”去执行。这样“点击”和“保存逻辑”解耦;且命令可以入队、记日志、支持撤销(保存命令带一个“撤销”方法,如恢复旧内容)。

所以,命令模式解决的是:把“请求”变成对象,从而支持排队、记录、撤销、宏(多命令组合)等,并让“谁触发”和“谁执行”解耦。

1.2 为什么需要命令模式?

场景一:需要撤销 / 重做

编辑器的“撤销”:每次操作(输入、删除、格式)都封装成一个命令对象,执行后把命令放进“历史”;撤销时从历史里取出上一个命令,执行其“撤销”方法。若请求没有对象化,就很难统一做撤销。

场景二:请求要排队、延迟执行

例如任务队列:很多“保存”“发送”请求先变成命令对象放进队列,再由工作线程按顺序执行。请求对象化后,可以统一管理、重试、延迟。

场景三:宏命令(一次执行多个操作)

把多个命令对象组合成一个“宏命令”,一次执行就依次执行多个命令,例如“一键部署” = 拉代码 + 安装依赖 + 重启服务,每个步骤是一个命令。

场景四:触发者与执行者解耦

按钮、菜单、快捷键不需要知道“具体调哪个类的哪个方法”,只负责“创建命令并交给执行者”;具体逻辑在命令里,命令里再调“接收者”。这样换一个接收者或换一种操作,只需换命令类,不用改按钮代码。

1.3 适用场景(什么时候用?)

场景 说明
撤销 / 重做 操作封装成命令,支持 undo/redo。
请求排队、日志、延迟执行 命令可存储、入队、记日志。
宏命令 多个命令组合成一条,一次执行。
解耦触发者与执行者 按钮/菜单只发命令,不关心谁执行、怎么执行。

简单记忆:要把请求当对象用(传、存、排队、撤销、组合)时,用命令模式。


二、命令模式的结构(四个角色)

角色 说明
命令(Command) 抽象接口,通常有 execute(),可选有 undo();表示“一个请求”。
具体命令(ConcreteCommand) 实现命令接口,内部持有接收者(或所需参数);execute() 里调用接收者的方法完成实际操作;若有撤销,则在 undo() 里做反向操作。
接收者(Receiver) 真正干活的类,提供具体方法(如开灯、关灯、保存)。命令对象在 execute 里调接收者。
触发者(Invoker) 持有命令对象,在某个时机(如按钮点击)调用 command.execute(),不关心命令内部怎么实现。

关系可以理解为:

  • 客户创建“具体命令”(并传入接收者或参数),把命令交给触发者(如遥控器、菜单)。
  • 触发者在适当时机调用 command.execute()
  • 具体命令的 execute 里调用 接收者 的方法,完成实际工作;需要时再实现 undo()

三、示例一:遥控器与灯(最经典)

需求:遥控器有一个按钮,按下去开灯或关灯。遥控器(触发者)不直接操作灯,而是持有一个“命令”对象,按下时执行命令;命令内部再调灯(接收者)。

3.1 接收者:灯

class Light:
    """接收者:灯"""
    def on(self):
        print("  灯 开")

    def off(self):
        print("  灯 关")

3.2 命令接口 + 具体命令

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

class LightOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.on()

class LightOffCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.off()

3.3 触发者:遥控器

class RemoteControl:
    """触发者:持有一个命令,按下时执行"""
    def __init__(self):
        self._command = None

    def set_command(self, command: Command):
        self._command = command

    def press_button(self):
        if self._command:
            self._command.execute()

3.4 使用

light = Light()
remote = RemoteControl()
remote.set_command(LightOnCommand(light))
remote.press_button()  # 灯 开

remote.set_command(LightOffCommand(light))
remote.press_button()  # 灯 关

要点:遥控器只认识“Command”,不认识 Light;换一个命令(如开风扇)只需换 set_command,不用改遥控器。


四、示例二:带撤销(Undo)

需求:灯可以开/关,遥控器要支持“撤销上一次操作”。命令除了 execute 还要有 undo;触发者记下“上一个命令”,撤销时调上一个命令的 undo。

4.1 命令接口带 undo

class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass

4.2 开灯/关灯命令的 undo = 反向操作

class LightOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.on()

    def undo(self):
        self._light.off()

class LightOffCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self):
        self._light.off()

    def undo(self):
        self._light.on()

4.3 触发者记录“上一个命令”

class RemoteControlWithUndo:
    def __init__(self):
        self._command = None
        self._last_command = None  # 用于撤销

    def set_command(self, command: Command):
        self._command = command

    def press_button(self):
        if self._command:
            self._command.execute()
            self._last_command = self._command

    def undo_button(self):
        if self._last_command:
            self._last_command.undo()
            self._last_command = None

4.4 使用

light = Light()
remote = RemoteControlWithUndo()
remote.set_command(LightOnCommand(light))
remote.press_button()  # 灯 开
remote.undo_button()   # 灯 关(撤销)

五、示例三:宏命令(一次执行多个命令)

需求:一键“全关”:关灯、关电视、关空调。把多个命令组合成一个“宏命令”,execute 时依次执行每个子命令。

class MacroCommand(Command):
    """宏命令:内部有多个命令,execute 时依次执行"""
    def __init__(self, commands: list):
        self._commands = commands

    def execute(self):
        for cmd in self._commands:
            cmd.execute()

接收者扩展:电视

class TV:
    def on(self):
        print("  电视 开")

    def off(self):
        print("  电视 关")

使用宏命令

light = Light()
tv = TV()
macro = MacroCommand([
    LightOffCommand(light),
    lambda: tv.off() if hasattr(tv, 'off') else None,  # 若 Command 不要求接口,可用 lambda
])
# 更规范:为 TV 也写 TvOffCommand,这里简化为直接包一层
class TvOffCommand(Command):
    def __init__(self, tv: TV):
        self._tv = tv
    def execute(self):
        self._tv.off()

macro = MacroCommand([LightOffCommand(light), TvOffCommand(tv)])
macro.execute()  # 灯 关 / 电视 关

六、示例四:文本编辑器中的撤销(保存状态)

需求:编辑器有“输入文字”操作,支持撤销(删掉刚输入的那段)。命令在执行时记录“被修改前的状态”,撤销时恢复。

class Editor:
    """接收者:编辑器内容"""
    def __init__(self):
        self.text = ""

    def add(self, s: str):
        self.text += s
        print("  内容:", self.text)

    def set_text(self, s: str):
        self.text = s

class AddTextCommand(Command):
    def __init__(self, editor: Editor, to_add: str):
        self._editor = editor
        self._to_add = to_add
        self._prev = None  # 撤销用:执行前的文本

    def execute(self):
        self._prev = self._editor.text
        self._editor.add(self._to_add)

    def undo(self):
        self._editor.set_text(self._prev)
        print("  撤销后:", self._editor.text)

这样每次“输入”都是一个命令,执行时记下旧内容,撤销时恢复旧内容;触发者(如菜单)只调 execute/undo,不关心编辑器内部实现。


七、命令模式 vs 其他模式

模式 目的 与命令的区别
命令 请求对象化,支持执行、撤销、排队、宏 强调“请求即对象”、解耦触发者与执行者。
策略 算法可替换 策略是“怎么做一件事”;命令是“做哪一件事”(可带参数、可撤销)。
备忘录 保存与恢复状态 常与命令配合:命令的 undo 用备忘录恢复状态。

简单记忆:命令 = 把“一次请求”封装成对象,可执行、可撤销、可排队、可组合。


八、常见问题与注意点

8.1 撤销的实现方式

  • 反向操作:如开灯 → 撤销即关灯;关灯 → 撤销即开灯。
  • 保存状态:执行前保存接收者状态,撤销时恢复(如编辑器文本)。按业务选择。

8.2 触发者要不要持有多条命令

可以。例如遥控器多个按键,每个按键持有一个命令;或一个按键支持“当前命令 + 上一个命令”(用于撤销)。按需求设计。

8.3 命令要不要带参数

可以。命令对象在创建时通过构造函数传入接收者和参数(如“开灯命令”带哪盏灯、“加文字命令”带要加的文字),execute 时直接用,触发者无需再传参。


九、小结

  • 命令模式:把请求封装成命令对象(带 execute,可选 undo);触发者只调用 command.execute(),具体命令内部调接收者完成实际工作;从而支持撤销、排队、宏,并解耦触发者与执行者
  • 四个角色:命令(接口)、具体命令(持接收者,实现 execute/undo)、接收者(真正干活)、触发者(持命令,在适当时机执行)。
  • 典型用途:遥控器/按钮、编辑器撤销、任务队列、宏命令等。
  • 实现要点:命令类持有接收者(或参数),execute 里调接收者;需要撤销时实现 undo(反向操作或恢复状态)。

建议先写“遥控器+灯”,再加“撤销”,最后试“宏命令”或“编辑器撤销”,这样对命令模式会掌握得比较扎实。

发表评论