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(反向操作或恢复状态)。
建议先写“遥控器+灯”,再加“撤销”,最后试“宏命令”或“编辑器撤销”,这样对命令模式会掌握得比较扎实。