`命令`模式:如何使用`Python`实现`可撤销`和`可重做`的操作。

好的,我们开始今天的讲座,主题是命令模式:如何使用 Python 实现可撤销和可重做操作

今天我们来深入探讨命令模式,以及如何利用它在 Python 中实现可撤销和可重做功能。这种模式在很多应用场景中都非常有用,例如文本编辑器、图形编辑器、游戏等等。

1. 命令模式的基本概念

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

简单来说,命令模式包含以下几个关键角色:

  • Command (命令):声明执行操作的接口。
  • ConcreteCommand (具体命令):实现 Command 接口,将一个接收者对象绑定于一个动作。调用接收者相应的操作,以实现 execute。
  • Receiver (接收者):知道如何实施与执行一个请求相关的操作。任何类都可充当一个接收者。
  • Invoker (调用者):要求该命令执行这个请求。
  • Client (客户):创建一个 ConcreteCommand 对象并设置其接收者。

2. 命令模式的优势

  • 解耦性:命令模式将发出请求的对象(调用者)和执行请求的对象(接收者)解耦。调用者无需知道接收者的具体实现,只需要知道如何执行命令即可。
  • 可扩展性:可以很容易地添加新的命令,而无需修改现有的代码。只需要创建新的 ConcreteCommand 类即可。
  • 可撤销/重做:命令模式可以很方便地实现撤销和重做功能,因为每个命令都包含了执行操作所需的所有信息。
  • 队列/日志:可以将命令存储在队列或日志中,用于后续的处理或分析。

3. Python 实现可撤销/重做的命令模式

下面我们通过一个简单的例子来说明如何在 Python 中实现可撤销/重做的命令模式。我们假设有一个简单的文本编辑器,可以执行插入文本和删除文本的操作。

class Command:
    """命令的抽象基类"""
    def execute(self):
        """执行命令"""
        raise NotImplementedError

    def undo(self):
        """撤销命令"""
        raise NotImplementedError

class InsertTextCommand(Command):
    """插入文本的命令"""
    def __init__(self, editor, text, position):
        self.editor = editor
        self.text = text
        self.position = position
        self.previous_text = None  # 用于撤销操作

    def execute(self):
        self.previous_text = self.editor.text  # 保存执行前的文本内容
        self.editor.insert_text(self.text, self.position)

    def undo(self):
        self.editor.text = self.previous_text

class DeleteTextCommand(Command):
    """删除文本的命令"""
    def __init__(self, editor, start, end):
        self.editor = editor
        self.start = start
        self.end = end
        self.deleted_text = None  # 用于撤销操作

    def execute(self):
        self.deleted_text = self.editor.text[self.start:self.end]  # 保存删除的文本
        self.editor.delete_text(self.start, self.end)

    def undo(self):
        self.editor.insert_text(self.deleted_text, self.start)  # 重新插入删除的文本

class TextEditor:
    """文本编辑器类,充当 Receiver"""
    def __init__(self):
        self.text = ""

    def insert_text(self, text, position):
        self.text = self.text[:position] + text + self.text[position:]
        print(f"Inserted '{text}' at position {position}. Current text: '{self.text}'")

    def delete_text(self, start, end):
        deleted_text = self.text[start:end]
        self.text = self.text[:start] + self.text[end:]
        print(f"Deleted '{deleted_text}' from position {start} to {end}. Current text: '{self.text}'")

class CommandInvoker:
    """命令调用者"""
    def __init__(self):
        self.history = []  # 存储执行过的命令
        self.redo_stack = [] # 存储撤销过的命令

    def execute(self, command):
        command.execute()
        self.history.append(command)
        self.redo_stack.clear()  # 执行新命令后清空重做栈

    def undo(self):
        if self.history:
            command = self.history.pop()
            command.undo()
            self.redo_stack.append(command) # 将撤销的命令加入重做栈

    def redo(self):
        if self.redo_stack:
            command = self.redo_stack.pop()
            command.execute()
            self.history.append(command) # 将重做的命令加入历史记录

# Client
editor = TextEditor()
invoker = CommandInvoker()

# 插入文本
insert_command1 = InsertTextCommand(editor, "Hello", 0)
invoker.execute(insert_command1)

insert_command2 = InsertTextCommand(editor, ", world!", 5)
invoker.execute(insert_command2)

# 删除文本
delete_command = DeleteTextCommand(editor, 5, 13)
invoker.execute(delete_command)

# 撤销操作
print("nUndoing...")
invoker.undo() # 撤销删除
print(f"Current text: '{editor.text}'")
invoker.undo() # 撤销插入", world!"
print(f"Current text: '{editor.text}'")
invoker.undo() # 撤销插入"Hello"
print(f"Current text: '{editor.text}'")
invoker.undo() # 没有更多命令可以撤销,不会报错

# 重做操作
print("nRedoing...")
invoker.redo() # 重做插入"Hello"
print(f"Current text: '{editor.text}'")
invoker.redo() # 重做插入", world!"
print(f"Current text: '{editor.text}'")
invoker.redo() # 重做删除", world!"
print(f"Current text: '{editor.text}'")
invoker.redo() # 没有更多命令可以重做,不会报错

print("nExecuting new command after redoing...")
insert_command3 = InsertTextCommand(editor, "!!!", 5)
invoker.execute(insert_command3) # 执行新的插入操作后,redo_stack被清空
print(f"Current text: '{editor.text}'")

print("nUndoing...")
invoker.undo() # 撤销插入"!!!"
print(f"Current text: '{editor.text}'")
invoker.undo() # 撤销删除", world!"
print(f"Current text: '{editor.text}'")

在这个例子中,Command 是一个抽象基类,定义了 executeundo 两个方法。InsertTextCommandDeleteTextCommand 是具体的命令类,分别实现了插入文本和删除文本的操作。TextEditor 类充当接收者,负责实际的文本操作。CommandInvoker 类充当调用者,负责执行命令和管理历史记录。redo_stack 用于存储撤销的命令,以便进行重做操作。

当调用 invoker.execute(command) 时,命令会被执行,并且被添加到 history 列表中。当调用 invoker.undo() 时,会从 history 列表中取出最后一个命令,并调用其 undo() 方法,从而撤销操作。同时,该命令会被放入 redo_stack 中。当调用 invoker.redo() 时, 会从 redo_stack 列表中取出最后一个命令,并调用其 execute() 方法,从而重做操作。同时,该命令会被放入 history 列表中。当执行新的命令时,redo_stack 会被清空。

4. 更复杂的命令参数处理

在实际应用中,命令可能需要传递更复杂的参数。例如,在图形编辑器中,移动图形的命令可能需要传递图形的 ID、移动的距离等参数。为了处理这些复杂的参数,可以将参数封装到一个单独的类中。

class MoveShapeCommand(Command):
    """移动图形的命令"""
    def __init__(self, editor, shape_id, delta_x, delta_y):
        self.editor = editor
        self.shape_id = shape_id
        self.delta_x = delta_x
        self.delta_y = delta_y

    def execute(self):
        self.editor.move_shape(self.shape_id, self.delta_x, self.delta_y)

    def undo(self):
        self.editor.move_shape(self.shape_id, -self.delta_x, -self.delta_y)

class GraphicsEditor:
    """图形编辑器类"""
    def __init__(self):
        self.shapes = {} # 存储图形,key是shape_id, value是坐标(x, y)
        self.next_shape_id = 1

    def create_shape(self, x, y):
        shape_id = self.next_shape_id
        self.shapes[shape_id] = (x, y)
        self.next_shape_id += 1
        print(f"Created shape with ID {shape_id} at ({x}, {y})")
        return shape_id

    def move_shape(self, shape_id, delta_x, delta_y):
        if shape_id in self.shapes:
            x, y = self.shapes[shape_id]
            self.shapes[shape_id] = (x + delta_x, y + delta_y)
            print(f"Moved shape with ID {shape_id} to ({x + delta_x}, {y + delta_y})")
        else:
            print(f"Shape with ID {shape_id} not found.")

    def delete_shape(self, shape_id):
        if shape_id in self.shapes:
            del self.shapes[shape_id]
            print(f"Deleted shape with ID {shape_id}")
        else:
            print(f"Shape with ID {shape_id} not found.")

class DeleteShapeCommand(Command):
    def __init__(self, editor, shape_id):
        self.editor = editor
        self.shape_id = shape_id
        self.shape_data = None # 保存删除图形的信息,用于撤销

    def execute(self):
        if self.shape_id in self.editor.shapes:
            x, y = self.editor.shapes[self.shape_id]
            self.shape_data = (x, y) # 保存图形的坐标
            self.editor.delete_shape(self.shape_id)
        else:
            print(f"Shape with ID {self.shape_id} not found.")

    def undo(self):
         if self.shape_data:
             x, y = self.shape_data
             self.editor.shapes[self.shape_id] = (x,y) # 重新创建图形
             print(f"Restored shape with ID {self.shape_id} to ({x}, {y})")
         else:
             print("No shape data to restore.")

# Client
graphics_editor = GraphicsEditor()
invoker = CommandInvoker()

# 创建图形
shape_id1 = graphics_editor.create_shape(10, 20)
shape_id2 = graphics_editor.create_shape(30, 40)

# 移动图形
move_command1 = MoveShapeCommand(graphics_editor, shape_id1, 5, 10)
invoker.execute(move_command1)

move_command2 = MoveShapeCommand(graphics_editor, shape_id2, -5, -10)
invoker.execute(move_command2)

# 删除图形
delete_command = DeleteShapeCommand(graphics_editor, shape_id1)
invoker.execute(delete_command)

# 撤销操作
print("nUndoing...")
invoker.undo()  # 撤销删除
invoker.undo()  # 撤销移动
invoker.undo()  # 撤销移动

# 重做操作
print("nRedoing...")
invoker.redo() # 重做移动
invoker.redo() # 重做移动
invoker.redo() # 重做删除

在这个例子中,MoveShapeCommand 接收图形的 ID 和移动的距离作为参数。DeleteShapeCommand 需要保存被删除图形的信息,以便 undo 操作可以恢复它。

5. 命令的合并和组合

有时候,我们需要将多个命令组合成一个复合命令。例如,在文本编辑器中,可以将 "加粗"、"斜体"、"下划线" 等操作组合成一个 "格式化文本" 命令。

class CompositeCommand(Command):
    """复合命令"""
    def __init__(self, commands):
        self.commands = commands

    def execute(self):
        for command in self.commands:
            command.execute()

    def undo(self):
        for command in reversed(self.commands):
            command.undo()

# 假设有 BoldCommand, ItalicCommand, UnderlineCommand 等命令

# 创建复合命令
# format_command = CompositeCommand([BoldCommand(editor), ItalicCommand(editor), UnderlineCommand(editor)])
# invoker.execute(format_command)
# invoker.undo(format_command)

CompositeCommand 类接收一个命令列表作为参数,并在 executeundo 方法中依次执行或撤销这些命令。

6. 命令模式的适用场景

  • 需要将请求排队或记录请求日志。
  • 需要支持可撤销/重做的操作。
  • 需要将调用者和接收者解耦。
  • 需要对请求进行参数化。

7. 命令模式的缺点

  • 可能会导致类的数量增加,特别是当命令种类较多时。
  • 命令对象可能会占用较多的内存,特别是当命令需要存储大量数据时。

8. 命令模式与其他模式的比较

  • 命令模式 vs 策略模式:策略模式关注的是算法的选择,而命令模式关注的是请求的封装。
  • 命令模式 vs 模板方法模式:模板方法模式定义算法的骨架,而命令模式将请求封装成对象。

9. 命令模式的进一步思考

  • 宏命令:可以将一系列命令录制成一个宏命令,然后一次性执行。
  • 远程执行:可以将命令序列化后通过网络发送到远程服务器执行。
  • 事务处理:可以使用命令模式来实现事务处理,确保一系列操作的原子性。

10. 总结:命令模式的核心价值

通过将请求封装为对象,命令模式实现了调用者和接收者的解耦,并为实现可撤销、可重做以及其他高级功能提供了可能。记住,合理运用设计模式可以使我们的代码更加灵活、可维护和可扩展。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注