21、Python - 命令模式

21、Python - 命令模式

你有没有遇到过这样的场景:写了一个文本编辑器,用户按 Ctrl+Z 想撤销上一步操作,你却发现代码里所有操作都是直接调用方法,根本没法回退?或者做了一个任务调度系统,想把请求排队、延迟执行、记录日志,却发现请求的发送者和执行者紧紧耦合在一起,改一处动全身?

这篇文章,我们就来彻底搞懂一个能完美解决这些问题的设计模式------命令模式。从是什么、为什么用、怎么演进而来,到企业级项目实战、与竞品模式对比、面试高频题,一文讲透,小白也能直接上手。


前言:为什么你一定要学命令模式

命令模式是 GoF 23 种设计模式中使用频率排名前三的行为型模式,几乎所有中大型软件系统里都能看到它的身影。Photoshop 的撤销重做、VS Code 的命令面板、游戏里的技能释放与回放、数据库事务的提交与回滚,底层都是命令模式在支撑。

学会它,你不仅能写出更优雅、更易扩展的代码,还能在面试中对答如流。更重要的是,它会改变你思考"操作"这件事的方式------原来"做一件事"本身也可以是一个对象,可以被存储、传递、排队、撤销、组合。


文章目录

一、痛点场景:那些让你抓狂的开发瞬间

场景一:文本编辑器的撤销噩梦

你接到一个需求:开发一个简易文本编辑器,支持输入文字、删除文字、替换文字,并且支持无限撤销和重做。

新手最容易写出这样的代码:

python 复制代码
class TextEditor:
    def __init__(self):
        self.content = ""

    def insert(self, text, position):
        self.content = self.content[:position] + text + self.content[position:]

    def delete(self, position, length):
        self.content = self.content[:position] + self.content[position + length:]

    def replace(self, position, length, new_text):
        self.content = self.content[:position] + new_text + self.content[position + length:]

看起来没问题对吧?但当产品经理说"加个撤销功能"时,你傻眼了。insert 操作直接改了 content,你根本不知道上一步改了什么,怎么撤销?难道要每次操作前都把整个 content 复制一份存起来?文本一大,内存直接爆炸。

场景二:智能家居遥控器的耦合地狱

你要做一个智能家居 App,一个遥控器上有 10 个按钮,分别控制灯、空调、电视、窗帘等设备。每个按钮按下时执行不同操作。

如果你这样写:

python 复制代码
class RemoteControl:
    def __init__(self, light, ac, tv):
        self.light = light
        self.ac = ac
        self.tv = tv

    def button_1_clicked(self):
        self.light.turn_on()

    def button_2_clicked(self):
        self.light.turn_off()

    def button_3_clicked(self):
        self.ac.turn_on()

    def button_4_clicked(self):
        self.ac.set_temperature(26)

问题来了:遥控器和具体设备紧紧绑在一起。加一个新设备就要改 RemoteControl 类,按钮和功能是硬编码的,用户想自定义按钮功能?不可能。想加一个"一键回家"宏,同时开灯、开空调、开电视?你得再写一个方法,里面依次调用。

场景三:任务队列与日志记录的困境

你在做一个订单系统,需要把用户的操作请求放入队列异步执行,并且记录所有操作日志,出问题时可以回放排查。

但你的请求就是一个函数调用,函数没法放进队列里序列化,也没法统一记录日志。你只能给每个函数都包一层 try/except 加日志,重复代码满天飞。

痛点总结

这三个场景的本质问题是一样的:请求的发送者和执行者直接耦合,"操作"本身没有被抽象成独立的对象。导致:

  • 无法撤销/重做操作
  • 无法排队、延迟执行请求
  • 无法统一记录操作日志
  • 无法组合多个操作形成宏命令
  • 新增操作需要修改调用者代码,违反开闭原则

二、痛点解决方案:把"操作"变成对象

命令模式的核心思想一句话就能说清:将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作

大白话翻译:以前你是"直接叫厨师炒菜",现在你是"把要炒什么菜写在一张订单上,交给服务员,服务员再把订单交给厨师"。这张订单就是命令对象。

这张订单有什么好处?

  • 订单可以攒着一起下单(排队执行)
  • 订单可以撕掉(撤销)
  • 订单可以复印留底(记录日志)
  • 多张订单可以合并成一个套餐(宏命令)
  • 服务员不需要知道菜怎么做,只需要传递订单(解耦)

三、命令模式是什么:五个角色一台戏

3.1 权威定义

GoF(四人组)在《设计模式:可复用面向对象软件的基础》中对命令模式的定义是:

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

命令模式属于行为型模式,关注的是对象之间职责的分配和通信方式。

3.2 五个核心角色

命令模式由五个角色组成,缺一不可:

角色 英文名 职责 大白话
命令接口 Command 声明执行操作的接口,通常有 execute() 方法,支持撤销时还有 undo() 订单的标准格式
具体命令 ConcreteCommand 实现命令接口,持有接收者引用,调用接收者的方法完成操作 具体的一张菜单
接收者 Receiver 真正执行命令的对象,任何类都可以是接收者 厨师,真正做菜的人
调用者 Invoker 持有命令对象,要求命令执行请求,是使用命令的入口 服务员,传递订单
客户端 Client 创建具体命令对象,组装命令和接收者 顾客,点菜的人

3.3 生活案例:餐厅点菜

用餐厅点菜来理解命令模式再合适不过了:

  • Command(命令接口):菜单的标准格式,都有菜名、做法、忌口
  • ConcreteCommand(具体命令):你点的"宫保鸡丁,少辣"这张订单
  • Receiver(接收者):后厨厨师,真正炒菜的人
  • Invoker(调用者):服务员,拿着订单传给后厨
  • Client(客户端):你,点菜的人

你(Client)对服务员(Invoker)说"来份宫保鸡丁",服务员把它写成订单(ConcreteCommand),送到后厨。厨师(Receiver)按单做菜。整个过程中,服务员不需要知道宫保鸡丁怎么做,你也不需要认识厨师,大家通过订单这个对象解耦了。

如果你说"那道菜不要了",服务员撕掉订单(撤销)。如果你说"把刚才那三道菜合成一个套餐",就是宏命令。


四、为什么要用命令模式:四大核心价值

4.1 解耦请求发送者和执行者

这是命令模式最根本的价值。调用者(Invoker)只知道命令接口,不知道具体是谁执行、怎么执行。接收者可以随时替换,调用者代码完全不用改。

4.2 支持撤销和重做

每个命令对象都封装了"做什么"和"怎么撤销"的信息。执行后把命令压入撤销栈,撤销时弹出并调用 undo(),同时压入重做栈。这是文本编辑器、绘图软件的标准实现方式。

4.3 支持请求排队和延迟执行

命令是对象,可以放进队列里。你可以实现一个命令队列,生产者往里放命令,消费者按顺序执行。这在任务调度、异步处理、网络请求中非常有用。

4.4 支持日志记录和事务回滚

每个命令执行前后都可以统一记录日志。如果一组命令中某个失败了,可以依次调用前面所有命令的 undo() 实现回滚,这就是数据库事务的基本原理。

4.5 更容易扩展

新增一个命令,只需要新增一个 ConcreteCommand 类,不需要修改 Invoker 和其他命令,完全符合开闭原则。


五、命令模式是怎么演进过来的

5.1 思想源头:回调函数

命令模式的思想可以追溯到很早的"回调函数"(Callback)。在 C 语言时代,人们就发现把函数指针作为参数传递,可以实现"在某个时刻执行某个操作"的需求。比如 qsort 函数的比较函数就是一种回调。

但函数指针有明显局限:它只能传递函数,不能携带状态和上下文,也没法实现撤销、排队等高级功能。

5.2 面向对象时代:命令对象

1980 年代到 1990 年代初,面向对象编程逐渐成熟。Smalltalk 语言中最早出现了"将操作封装为对象"的实践,被称为"Command"或"Action"对象。Macintosh 的 GUI 系统中,菜单和按钮的事件处理就使用了类似的思想。

5.3 GoF 正式定名:1994 年

1994 年,Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides 四人(俗称 GoF,四人组)出版了《Design Patterns: Elements of Reusable Object-Oriented Software》一书,首次系统地整理了 23 种设计模式,命令模式就是其中之一,被归类为行为型模式。

这本书的出版标志着设计模式从经验总结变成了系统化的工程方法论,命令模式也从此成为软件开发中的经典模式。

5.4 现代应用:从桌面软件到分布式系统

随着软件架构的演进,命令模式的应用场景不断扩展:

  • 1990s-2000s:桌面软件的撤销重做(Microsoft Office、Adobe Photoshop)
  • 2000s-2010s:Web 框架中的命令总线(CQRS 模式的基础)
  • 2010s-至今:分布式系统中的事件溯源(Event Sourcing)、任务队列、游戏回放、AI Agent 的工具调用

值得一提的是,现在大火的 AI Agent 中,LLM 选择工具并执行的过程,本质上也是命令模式的思想------工具调用就是一个命令对象,Agent 是 Invoker,工具是 Receiver。


六、命令模式怎么用:从入门到企业级

6.1 基础实现:Python 版经典结构

先来看最经典的命令模式实现,对应网页中的代码,我做了一些改进和注释:

python 复制代码
from abc import ABC, abstractmethod


# ========== Command:命令接口 ==========
class Command(ABC):
    """命令模式接口,所有具体命令都要实现这个接口"""

    @abstractmethod
    def execute(self):
        """执行命令"""
        pass

    def undo(self):
        """撤销命令,默认不支持撤销,子类按需重写"""
        raise NotImplementedError("该命令不支持撤销")


# ========== Receiver:接收者 ==========
class Receiver:
    """真正执行操作的对象,任何类都可以成为接收者"""

    def __init__(self, name):
        self.name = name

    def action(self):
        """接收者的实际业务方法"""
        print(f"[{self.name}] 执行了操作")


# ========== ConcreteCommand:具体命令 ==========
class ConcreteCommand(Command):
    """具体命令,持有接收者引用,调用接收者的方法"""

    def __init__(self, receiver: Receiver):
        self.receiver = receiver

    def execute(self):
        self.receiver.action()

    def undo(self):
        print(f"撤销了对 {self.receiver.name} 的操作")


# ========== Invoker:调用者 ==========
class Invoker:
    """调用者,持有命令对象,是使用命令的入口"""

    def __init__(self):
        self._commands = []

    def add_command(self, cmd: Command):
        """添加命令到队列"""
        self._commands.append(cmd)

    def remove_command(self, cmd: Command):
        """从队列移除命令"""
        self._commands.remove(cmd)

    def run_command(self):
        """依次执行所有命令"""
        for cmd in self._commands:
            cmd.execute()


# ========== Client:客户端 ==========
def client():
    # 1. 创建接收者
    receiver1 = Receiver("命令一")
    receiver2 = Receiver("命令二")
    receiver3 = Receiver("命令三")

    # 2. 创建具体命令,绑定接收者
    cmd1 = ConcreteCommand(receiver1)
    cmd2 = ConcreteCommand(receiver2)
    cmd3 = ConcreteCommand(receiver3)

    # 3. 创建调用者,添加命令
    invoker = Invoker()
    invoker.add_command(cmd1)
    invoker.add_command(cmd2)
    invoker.add_command(cmd3)

    # 4. 执行命令
    invoker.run_command()


if __name__ == "__main__":
    client()

运行输出:

text 复制代码
[命令一] 执行了操作
[命令二] 执行了操作
[命令三] 执行了操作

6.2 进阶实现:支持撤销重做的文本编辑器

现在来解决前面痛点场景一中的问题。用命令模式实现一个支持撤销重做的文本编辑器:

python 复制代码
from abc import ABC, abstractmethod
from collections import deque


# ========== Command 接口 ==========
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @abstractmethod
    def undo(self):
        pass


# ========== Receiver:文档 ==========
class Document:
    """文档类,真正存储和操作内容的接收者"""

    def __init__(self):
        self.content = ""

    def insert_text(self, text, position):
        self.content = self.content[:position] + text + self.content[position:]

    def delete_text(self, position, length):
        return self.content[position:position + length]
        self.content = self.content[:position] + self.content[position + length:]

    def replace_text(self, position, length, new_text):
        old_text = self.content[position:position + length]
        self.content = self.content[:position] + new_text + self.content[position + length:]
        return old_text


# ========== ConcreteCommand:插入命令 ==========
class InsertCommand(Command):
    def __init__(self, document: Document, text: str, position: int):
        self.document = document
        self.text = text
        self.position = position

    def execute(self):
        self.document.insert_text(self.text, self.position)

    def undo(self):
        # 撤销插入 = 删除插入的文本
        self.document.delete_text(self.position, len(self.text))


# ========== ConcreteCommand:删除命令 ==========
class DeleteCommand(Command):
    def __init__(self, document: Document, position: int, length: int):
        self.document = document
        self.position = position
        self.length = length
        self._deleted_text = ""  # 保存被删除的文本,用于撤销

    def execute(self):
        self._deleted_text = self.document.content[self.position:self.position + self.length]
        self.document.delete_text(self.position, self.length)

    def undo(self):
        # 撤销删除 = 把删除的文本插回去
        self.document.insert_text(self._deleted_text, self.position)


# ========== Invoker:命令管理器(带撤销重做栈) ==========
class CommandManager:
    """命令管理器,维护撤销栈和重做栈"""

    def __init__(self, max_history: int = 100):
        self._undo_stack = deque(maxlen=max_history)
        self._redo_stack = deque(maxlen=max_history)

    def execute(self, cmd: Command):
        """执行命令并压入撤销栈,同时清空重做栈"""
        cmd.execute()
        self._undo_stack.append(cmd)
        self._redo_stack.clear()  # 新操作后重做栈清空

    def undo(self):
        """撤销:从撤销栈弹出,执行undo,压入重做栈"""
        if not self._undo_stack:
            print("没有可撤销的操作")
            return
        cmd = self._undo_stack.pop()
        cmd.undo()
        self._redo_stack.append(cmd)

    def redo(self):
        """重做:从重做栈弹出,执行execute,压回撤销栈"""
        if not self._redo_stack:
            print("没有可重做的操作")
            return
        cmd = self._redo_stack.pop()
        cmd.execute()
        self._undo_stack.append(cmd)


# ========== 使用示例 ==========
def main():
    doc = Document()
    manager = CommandManager()

    # 执行一系列操作
    manager.execute(InsertCommand(doc, "Hello ", 0))
    print(f"插入后: '{doc.content}'")

    manager.execute(InsertCommand(doc, "World", 6))
    print(f"再插入后: '{doc.content}'")

    manager.execute(DeleteCommand(doc, 0, 6))
    print(f"删除后: '{doc.content}'")

    # 撤销
    manager.undo()
    print(f"撤销删除后: '{doc.content}'")

    manager.undo()
    print(f"撤销插入后: '{doc.content}'")

    # 重做
    manager.redo()
    print(f"重做插入后: '{doc.content}'")


if __name__ == "__main__":
    main()

运行输出:

text 复制代码
插入后: 'Hello '
再插入后: 'Hello World'
删除后: 'World'
撤销删除后: 'Hello World'
撤销插入后: 'Hello '
重做插入后: 'Hello World'

完美实现了撤销和重做,而且每个命令只保存增量信息(插入了什么、删除了什么),不会像复制整个文档那样浪费内存。

6.3 高级实现:宏命令(批量操作)

宏命令就是把多个命令组合成一个命令,执行时依次执行所有子命令,撤销时逆序撤销。这在"一键回家""批量处理"等场景非常有用。

python 复制代码
from abc import ABC, abstractmethod


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

    @abstractmethod
    def undo(self):
        pass


class MacroCommand(Command):
    """宏命令:组合多个命令为一个命令"""

    def __init__(self, commands: list = None):
        self.commands = commands or []

    def add(self, cmd: Command):
        self.commands.append(cmd)

    def remove(self, cmd: Command):
        self.commands.remove(cmd)

    def execute(self):
        """正序执行所有子命令"""
        for cmd in self.commands:
            cmd.execute()

    def undo(self):
        """逆序撤销所有子命令"""
        for cmd in reversed(self.commands):
            cmd.undo()


# ========== 智能家居示例 ==========
class Light:
    def turn_on(self):
        print("灯已打开")

    def turn_off(self):
        print("灯已关闭")


class AirConditioner:
    def turn_on(self):
        print("空调已打开")

    def set_temperature(self, temp):
        print(f"空调温度设为 {temp}度")

    def turn_off(self):
        print("空调已关闭")


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

    def execute(self):
        self.light.turn_on()

    def undo(self):
        self.light.turn_off()


class ACOnCommand(Command):
    def __init__(self, ac: AirConditioner, temp: int = 26):
        self.ac = ac
        self.temp = temp

    def execute(self):
        self.ac.turn_on()
        self.ac.set_temperature(self.temp)

    def undo(self):
        self.ac.turn_off()


def main():
    light = Light()
    ac = AirConditioner()

    # 创建"一键回家"宏命令
    home_mode = MacroCommand()
    home_mode.add(LightOnCommand(light))
    home_mode.add(ACOnCommand(ac, temp=26))

    print("=== 执行一键回家 ===")
    home_mode.execute()

    print("\n=== 撤销一键回家 ===")
    home_mode.undo()


if __name__ == "__main__":
    main()

运行输出:

text 复制代码
=== 执行一键回家 ===
灯已打开
空调已打开
空调温度设为 26度

=== 撤销一键回家 ===
空调已关闭
灯已关闭

6.4 企业级实现:命令队列与异步任务调度

在真实企业项目中,命令模式常被用来实现任务队列。下面是一个简化版的异步命令队列,支持优先级、重试、日志记录:

python 复制代码
import time
import threading
import logging
from abc import ABC, abstractmethod
from queue import PriorityQueue
from dataclasses import dataclass, field

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)


# ========== Command 接口 ==========
class Command(ABC):
    @abstractmethod
    def execute(self):
        pass

    @property
    def name(self):
        return self.__class__.__name__


# ========== 带优先级的命令包装 ==========
@dataclass(order=True)
class PrioritizedCommand:
    priority: int
    cmd: Command = field(compare=False)
    retries: int = field(default=0, compare=False)
    max_retries: int = field(default=3, compare=False)


# ========== Invoker:异步命令队列 ==========
class AsyncCommandQueue:
    """企业级异步命令队列,支持优先级、重试、日志"""

    def __init__(self, worker_count: int = 2):
        self._queue = PriorityQueue()
        self._workers = []
        self._stop_event = threading.Event()
        self._worker_count = worker_count

    def start(self):
        """启动工作线程"""
        for i in range(self._worker_count):
            t = threading.Thread(target=self._worker_loop, args=(i,), daemon=True)
            t.start()
            self._workers.append(t)
        logger.info(f"命令队列已启动,{self._worker_count} 个工作线程")

    def stop(self):
        """停止队列,等待所有任务完成"""
        self._queue.join()
        self._stop_event.set()
        logger.info("命令队列已停止")

    def submit(self, cmd: Command, priority: int = 5, max_retries: int = 3):
        """提交命令到队列,priority 越小优先级越高"""
        p_cmd = PrioritizedCommand(priority=priority, cmd=cmd, max_retries=max_retries)
        self._queue.put(p_cmd)
        logger.info(f"命令 [{cmd.name}] 已提交,优先级: {priority}")

    def _worker_loop(self, worker_id: int):
        """工作线程主循环"""
        while not self._stop_event.is_set():
            try:
                p_cmd = self._queue.get(timeout=0.5)
            except Exception:
                continue

            cmd = p_cmd.cmd
            try:
                logger.info(f"[Worker-{worker_id}] 开始执行命令 [{cmd.name}]")
                start = time.time()
                cmd.execute()
                elapsed = time.time() - start
                logger.info(f"[Worker-{worker_id}] 命令 [{cmd.name}] 执行成功,耗时 {elapsed:.3f}s")
                self._queue.task_done()
            except Exception as e:
                logger.error(f"[Worker-{worker_id}] 命令 [{cmd.name}] 执行失败: {e}")
                if p_cmd.retries < p_cmd.max_retries:
                    p_cmd.retries += 1
                    logger.info(f"命令 [{cmd.name}] 第 {p_cmd.retries} 次重试")
                    self._queue.put(p_cmd)
                else:
                    logger.error(f"命令 [{cmd.name}] 达到最大重试次数,已放弃")
                self._queue.task_done()


# ========== 具体命令示例 ==========
class SendEmailCommand(Command):
    def __init__(self, to: str, subject: str, body: str):
        self.to = to
        self.subject = subject
        self.body = body

    def execute(self):
        time.sleep(0.5)  # 模拟发送邮件耗时
        logger.info(f"发送邮件给 {self.to}: {self.subject}")


class GenerateReportCommand(Command):
    def __init__(self, report_type: str):
        self.report_type = report_type

    def execute(self):
        time.sleep(1.0)  # 模拟生成报表耗时
        logger.info(f"生成 {self.report_type} 报表完成")


# ========== 使用示例 ==========
def main():
    queue = AsyncCommandQueue(worker_count=2)
    queue.start()

    # 提交不同优先级的命令
    queue.submit(SendEmailCommand("admin@example.com", "紧急告警", "服务器CPU过高"), priority=1)
    queue.submit(GenerateReportCommand("月度销售"), priority=5)
    queue.submit(SendEmailCommand("user@example.com", "欢迎注册", "感谢注册"), priority=10)

    time.sleep(4)  # 等待任务执行
    queue.stop()


if __name__ == "__main__":
    main()

这个企业级队列的特点:

  • 支持优先级调度,紧急任务先执行
  • 支持失败自动重试,达到最大次数后放弃
  • 每个命令执行前后统一记录日志
  • 多线程并发执行,提高吞吐量
  • 命令和执行逻辑完全解耦,新增命令只需实现 Command 接口

七、常用场景教学:命令模式的六大应用场景

场景一:GUI 按钮与菜单绑定

这是命令模式最经典的应用。界面上的每个按钮、菜单项都绑定一个 Command 对象,点击时调用 execute()。按钮不需要知道具体做什么,只需要调用命令。更换按钮功能只需要替换绑定的命令对象。

python 复制代码
class Button:
    def __init__(self, label):
        self.label = label
        self._command = None

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

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

场景二:撤销重做系统

文本编辑器、绘图软件、IDE 等所有需要撤销功能的应用,底层都是命令模式。每个操作封装为命令,执行后压入撤销栈,撤销时调用 undo()。

场景三:任务队列与异步处理

把命令对象放入队列,由消费者线程异步执行。适用于邮件发送、报表生成、数据同步等耗时操作。前面的企业级示例就是这个场景。

场景四:宏命令与批量操作

把多个命令组合成一个宏命令,一键执行。比如 Photoshop 的"动作"功能、Excel 的宏、智能家居的"一键回家/离家"模式。

场景五:事务与回滚

一组命令要么全部成功,要么全部回滚。执行时依次执行,如果中间某个失败,逆序调用已执行命令的 undo()。这就是数据库事务、分布式事务的基本原理。

python 复制代码
class Transaction:
    def __init__(self):
        self._commands = []
        self._executed = []

    def add(self, cmd: Command):
        self._commands.append(cmd)

    def commit(self):
        try:
            for cmd in self._commands:
                cmd.execute()
                self._executed.append(cmd)
        except Exception as e:
            self.rollback()
            raise e

    def rollback(self):
        for cmd in reversed(self._executed):
            cmd.undo()
        self._executed.clear()

场景六:操作日志与审计回放

每个命令执行时记录日志,出问题时可以根据日志回放所有操作,排查问题。这在金融系统、运维系统中非常重要。


八、企业项目中如何使用命令模式:实战经验

8.1 什么时候该用,什么时候不该用

该用的信号:

  • 需要撤销/重做功能
  • 需要把操作排队、延迟执行
  • 需要统一记录操作日志
  • 需要组合多个操作为宏
  • 调用者和执行者需要解耦
  • 需要支持事务回滚

不该用的信号:

  • 操作非常简单,只有一两个方法调用
  • 不需要撤销、排队等高级功能
  • 系统很小,不会扩展
  • 为了用模式而用模式,导致类爆炸

记住:设计模式是解决问题的工具,不是目的。简单场景直接调用方法就好,不要过度设计。

8.2 Python 中的简化写法

Python 是动态语言,实现命令模式可以比 Java 简洁很多。对于简单场景,甚至可以用函数或可调用对象代替命令类:

python 复制代码
# 极简版:用函数作为命令
class SimpleInvoker:
    def __init__(self):
        self._commands = []

    def add(self, func, *args, **kwargs):
        self._commands.append((func, args, kwargs))

    def run(self):
        for func, args, kwargs in self._commands:
            func(*args, **kwargs)


# 使用
def greet(name):
    print(f"Hello, {name}")

invoker = SimpleInvoker()
invoker.add(greet, "Alice")
invoker.add(greet, "Bob")
invoker.run()

但如果需要撤销、排队、优先级等高级功能,还是建议用完整的命令类,因为函数无法携带 undo 信息和状态。

8.3 与其他模式的组合使用

在企业项目中,命令模式很少单独使用,通常和其他模式组合:

  • + 工厂模式:用工厂创建命令对象,封装命令的创建逻辑
  • + 责任链模式:命令沿责任链传递,直到被合适的处理器执行
  • + 备忘录模式:用备忘录保存命令执行前的状态,实现更复杂的撤销
  • + 观察者模式:命令执行后通知观察者,实现事件驱动
  • + 策略模式:命令内部使用策略选择具体算法

8.4 性能与内存注意事项

  • 撤销栈不要无限增长,设置最大长度(如前面的 deque(maxlen=100))
  • 命令对象尽量轻量,只保存必要的增量信息,不要保存大对象的完整副本
  • 异步队列中注意线程安全,Python 的 queue 模块是线程安全的
  • 宏命令注意子命令的执行顺序和撤销顺序

九、竞品对比:命令模式 vs 策略模式 vs 状态模式 vs 备忘录模式

命令模式经常和其他几个行为型模式搞混,这里用表格彻底讲清它们的区别。

9.1 核心区别对比表

对比维度 命令模式 Command 策略模式 Strategy 状态模式 State 备忘录模式 Memento
核心思想 把请求封装为对象,支持撤销、排队 把算法封装为对象,可互换 行为随内部状态改变 保存对象状态,用于恢复
关键方法 execute() + undo() execute() / algorithm() handle() + transition() setState() / getState()
关注点 "做什么"这个操作本身 "怎么做"的算法选择 "什么状态下做什么" "之前是什么状态"
是否有生命周期 有,可执行、撤销、排队 无,调用一次算一次 有,状态之间有转换规则 无,只是存数据
谁决定行为 客户端创建命令,调用者触发 客户端选择策略 状态内部自动转换 原发器创建,看守者保存
典型场景 撤销重做、任务队列、宏命令 支付方式、排序算法、折扣策略 订单状态机、游戏角色、连接状态 游戏存档、编辑器快照、数据库回滚点
类数量 每个操作一个命令类 每个算法一个策略类 每个状态一个状态类 通常一个备忘录类
一句话区分 命令有"执行/撤销"生命周期 策略只"算一次",算完就完 状态之间有转换规则,自动变行为 只存状态,不存操作

9.2 命令模式 vs 策略模式

这是最容易搞混的一对。它们的结构很像,都有一个接口和多个实现类,都通过组合来委托行为。

核心区别:

  • 命令模式封装的是"请求/操作",有 execute 和 undo,关心操作的生命周期(执行、撤销、排队)
  • 策略模式封装的是"算法",只有一个方法,关心算法的可替换性,没有撤销的概念

举例:

  • 你点了一份外卖(命令),可以取消(撤销),可以攒着一起下单(排队)------这是命令模式
  • 你选择用什么支付方式(策略),微信、支付宝、银行卡,选一个付完就完了,没有"撤销支付方式"的说法------这是策略模式

9.3 命令模式 vs 状态模式

  • 命令模式中,命令是由客户端创建并传入的,客户端决定执行什么命令
  • 状态模式中,状态是对象内部维护的,状态的转换由内部逻辑自动完成,客户端不直接控制状态切换

举例:

  • 遥控器按哪个键由你决定,每个键是一个命令------命令模式
  • 订单从"待付款"到"已付款"到"已发货",状态转换有固定规则,不是你想跳就跳------状态模式

9.4 命令模式 vs 备忘录模式

  • 命令模式通过记录"做了什么操作"来实现撤销,每个命令知道怎么 undo
  • 备忘录模式通过保存"操作前的完整状态"来实现恢复,恢复时直接把状态覆盖回去

举例:

  • 你在文本里输入了"Hello",命令模式记录"在位置0插入了Hello",撤销时删除它------命令模式
  • 你玩游戏存了个档,备忘录模式保存了当前全部游戏状态,读档时直接恢复到存档时的状态------备忘录模式

选择建议: 操作是增量的、可计算逆操作的,用命令模式更省内存;操作很复杂、难以计算逆操作的,用备忘录模式更简单。


十、面试官高频面试题

Q1:什么是命令模式?它的核心思想是什么?

参考答案:

命令模式是一种行为型设计模式,核心思想是将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。

简单说就是把"做一件事"这个操作本身变成一个对象,这个对象可以被传递、存储、排队、撤销、组合。命令模式包含五个角色:Command(命令接口)、ConcreteCommand(具体命令)、Receiver(接收者)、Invoker(调用者)、Client(客户端)。

Q2:命令模式和策略模式有什么区别?

参考答案:

两者结构相似但意图完全不同:

  1. 封装的内容不同:命令模式封装的是"请求/操作",策略模式封装的是"算法"
  2. 生命周期不同:命令有 execute 和 undo,有执行、撤销、排队等生命周期;策略只有一个算法方法,调用一次算一次
  3. 关注点不同:命令模式关注操作的解耦、撤销、排队;策略模式关注算法的可替换性
  4. 举例:外卖订单可以取消、可以攒单,是命令模式;选择支付方式,选一个付完就完,是策略模式

Q3:命令模式如何实现撤销和重做?

参考答案:

使用两个栈:撤销栈(undo stack)和重做栈(redo stack)。

  1. 执行新命令时,调用 execute(),然后把命令压入撤销栈,同时清空重做栈
  2. 撤销时,从撤销栈弹出命令,调用 undo(),然后压入重做栈
  3. 重做时,从重做栈弹出命令,调用 execute(),然后压回撤销栈
  4. 每个具体命令类需要实现 execute() 和 undo() 两个方法,undo 是 execute 的逆操作

关键是命令对象要保存足够的状态信息来支持逆操作,比如插入命令要保存插入的位置和内容,删除命令要保存删除的位置和被删除的内容。

Q4:命令模式的优缺点是什么?

参考答案:

优点:

  1. 降低系统耦合度,请求发送者和执行者解耦
  2. 新命令容易添加,符合开闭原则
  3. 支持撤销、重做、排队、日志、宏命令等高级功能
  4. 可以组合多个命令形成复合命令

缺点:

  1. 可能导致系统有过多的具体命令类,每个操作一个类,增加代码复杂度
  2. 增加了系统的抽象层次,对于简单操作来说有些过度设计
  3. 命令类的数量可能随操作增多而膨胀,需要合理管理

Q5:命令模式在实际项目中有哪些应用场景?

参考答案:

  1. GUI 按钮和菜单的事件处理,每个按钮绑定一个命令对象
  2. 文本编辑器、绘图软件的撤销重做功能
  3. 任务队列和异步处理,把命令放入队列由消费者执行
  4. 宏命令和批量操作,如 Photoshop 动作、智能家居场景模式
  5. 数据库事务和回滚,一组命令要么全成功要么全撤销
  6. 操作日志和审计回放,记录每个命令用于问题排查
  7. 游戏中的技能释放、操作回放、AI 行为控制
  8. 分布式系统中的 CQRS(命令查询职责分离)和 Event Sourcing(事件溯源)

Q6:什么是宏命令?如何实现?

参考答案:

宏命令(Macro Command)是组合模式在命令模式中的应用,它把多个命令组合成一个复合命令。

实现方式:宏命令类实现 Command 接口,内部维护一个命令列表。execute() 方法正序执行所有子命令,undo() 方法逆序撤销所有子命令。还可以提供 add()、remove() 方法来管理子命令。

宏命令的典型应用:Photoshop 的"动作"功能、Excel 宏、智能家居的"一键回家"场景(同时开灯、开空调、开电视)。

Q7:命令模式和备忘录模式都能实现撤销,有什么区别?

参考答案:

两者实现撤销的方式不同:

  1. 命令模式:记录"做了什么操作",每个命令知道自己的逆操作(undo),撤销时执行逆操作。优点是节省内存(只存增量信息),缺点是需要为每个命令实现逆操作,复杂操作的逆操作可能很难计算。

  2. 备忘录模式:保存"操作前的完整状态",撤销时直接用保存的状态覆盖当前状态。优点是实现简单,不需要计算逆操作,缺点是内存开销大(每次保存完整状态)。

选择建议:操作是增量的、逆操作容易计算的用命令模式;操作复杂、难以计算逆操作、对内存不敏感的用备忘录模式。实际项目中两者可以结合使用。

Q8:Python 中实现命令模式有什么简化方式?

参考答案:

Python 是动态语言,实现命令模式比 Java 灵活:

  1. 简单场景可以直接用函数或 lambda 作为命令,因为 Python 中函数是一等公民,可以像对象一样传递和存储
  2. 可以用可调用对象(实现 call 方法的类)作为命令,比定义 execute 方法更 Pythonic
  3. 对于需要撤销的场景,还是建议用完整的命令类,因为函数无法携带 undo 方法和状态
  4. 可以用 dataclass 来定义命令类,减少样板代码
  5. 用 collections.deque 实现撤销栈,自动限制最大长度

但要注意:简化是为了更清晰,不是为了炫技。如果团队主要是 Java 背景,或者需要明确的接口约束,还是用经典的类结构更好。


十一、总结

命令模式的本质是将"操作"抽象为对象,从而赋予操作对象才有的能力:可以传递、可以存储、可以排队、可以撤销、可以组合。

回顾一下全文要点:

  1. 是什么:把请求封装为对象,五个角色(Command、ConcreteCommand、Receiver、Invoker、Client)
  2. 为什么用:解耦、撤销重做、排队延迟、日志事务、易扩展
  3. 怎么演进:从回调函数到命令对象,1994年GoF正式定名,现在应用于分布式系统和AI Agent
  4. 怎么用:基础实现到企业级异步队列,支持撤销重做和宏命令
  5. 常用场景:GUI绑定、撤销重做、任务队列、宏命令、事务回滚、日志审计
  6. 竞品对比:命令有生命周期,策略只算一次,状态自动转换,备忘录只存状态
  7. 面试题:八个高频题,从定义到实现到对比全覆盖

最后送你一句话:设计模式不是银弹,而是工具箱。遇到匹配的问题时拿出来用,不要为了用模式而用模式。 命令模式虽好,但如果你的操作只是简单调用一个方法,不需要撤销、排队等功能,直接调用就好,简单才是美。


转载声明

本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
astronautyi37 分钟前
Go 运行时内存分配与 GC 位图深度剖析
开发语言·后端·golang
AI推荐率40 分钟前
企业归属关系:开发、运营、持有和销售主体需要按真实关系表达
人工智能
海兰40 分钟前
【应用】Firecrawl-Style Scrape Demo网页爬取效果演示及部署
人工智能·网页爬虫
2601_9620779841 分钟前
机器学习及其Python实践
pytorch·python·机器学习·tensorflow·scikit-learn
code 小楊42 分钟前
Agent间如何高效协作?深度拆解A2A(Agent-to-Agent)协议
人工智能·架构·开源
天远Date Lab44 分钟前
计算机视觉管道构建:基于天远身份证OCR实现机器学习身份信息提取
人工智能·机器学习·计算机视觉·ocr
techdashen44 分钟前
Go 中如何处理错误
开发语言·后端·golang
俊哥V1 小时前
AI一周事件 · 2026-08-26 至 2026-09-01
人工智能·ai
Wang's Blog1 小时前
Vibe Coding一人即团队系列50: 基于 Claude Code 的多项目测试计划生成与自动化测试实战
人工智能