# 第 11 章 命令模式

撤销操作是在 1974 年引入的，但 Fortran 和 Lisp 分别早在 1957 年和 1958 年就已创建了撤销操作。

推荐使用命令模式（Command pattern）来实现撤销。

命令设计模式帮助我们将一个操作（撤销、重做、复制、粘贴等）封装成一个对象。简而言之，这意味着创建一个类，包含实现该操作所需要的所有逻辑和方法。这样做的优势如下所述：

* 我们并不需要直接执行一个命令。命令可以按照希望执行。
* 调用命令的对象与知道如何执行命令的对象解耦。调用者无需知道命令的任何实现细节。
* 如果有意义，可以把多个命令组织起来，这样调用者能够按顺序执行它们。例如，在实现一个多层撤销命令时，这是很有用的。

## 11.1 现实生活的例子

顾客 -> 服务员 -> 订单 -> 厨师

## 11.2 软件的例子

PyQt 是 QT 工具包的 Python 绑定。PyQt 包含一个 QAction 类，将一个动作建模为一个命令。对每个动作都支持额外的可选信息， 比如， 描述、工具提示、快捷键和其他。

git-cola 是使用 Python 语言编写的一个 Git GUI，它使用命令模式来修改模型、变更一次提交、应用一个差异选择、签出，等等。

## 11.3 应用案例

许多开发人员以为撤销例子是命令模式的唯一应用案例。撤销操作确实是命令模式的杀手级特性，然而命令模式能做的实际上还有很多：

* GUI 按钮和菜单项：前面提过的 PyQt 例子使用命令模式来实现按钮和菜单项上的动作。
* 其他操作：除了撤销，命令模式可用于实现任何操作。其中一些例子包括剪切、复制、粘贴、重做和文本大写。
* 事务型行为和日志记录：事务型行为和日志记录对于为变更记录一份持久化日志是很重要的。操作系统用它来从系统崩溃中恢复，关系型数据库用它来实现事务，文件系统用它来实现快照，而安装程序（向导程序）用它来恢复取消的安装。
* 宏：在这里，宏是指一个动作序列，可在任意时间点按要求进行录制和执行。流行的编辑器（比如，Emacs 和 Vim）都支持宏。

## 11.4 实现

本节中，我们将使用命令模式实现最基本的文件操作工具：

* 创建一个文件，并随意写入一个字符串
* 读取一个文件的内容
* 重命名一个文件
* 删除一个文件

下面实现的创建文件和重命名文件支持撤销，删除文件不支持。但对于文件删除操作实际上是可以实现撤销的，一种技术是使用一个特殊的垃圾箱/废物篓目录来存储所有被删除文件，这样在用户请求时可以恢复出来。

```python
import os

verbose = True    # 全局设定，默认提供详细交互信息

# 重命名文件，包含撤销操作
class RenameFile:
    def __init__(self, path_src, path_dest):
        self.src, self.dest = path_src, path_dest
    def execute(self):
        if verbose:
            print("[renaming '{}' to '{}']".format(self.src, self.dest))
        os.rename(self.src, self.dest)
    def undo(self):
        if verbose:
            print("[renaming '{}' back to '{}']".format(self.dest, self.src))
        os.rename(self.dest, self.src)

# 创建文件， 包含撤销操作
class CreateFile:
    def __init__(self, path, txt='hello world\n'):
        self.path, self.txt = path, txt
    def execute(self):
        if verbose:
            print("[creating file '{}']".format(self.path))
        with open(self.path, mode='w', encoding='utf-8') as out_file:
            out_file.write(self.txt)
    def undo(self):
        delete_file(self.path)

# 读取文件内容，没有撤销操作
class ReadFile:
    def __init__(self, path):
        self.path = path
    def execute(self):
        if verbose:
            print("[reading file '{}']".format(self.path))
        with open(self.path, mode='r', encoding='utf-8') as in_file:
            print(in_file.read(), end='')

# 删除文件，没有撤销操作
def delete_file(path):
    if verbose:
        print("deleting file '{}'".format(path))
    os.remove(path)


def main():
    orig_name, new_name = 'file1', 'file2'

    commands = []
    for cmd in CreateFile(orig_name), ReadFile(orig_name), RenameFile(orig_name, new_name):
        commands.append(cmd)

    [c.execute() for c in commands]

    answer = input('reverse the executed commands? [y/n] ')

    if answer not in 'yY':
        print("the result is {}".format(new_name))
        exit()

    for c in reversed(commands):
        try:
            c.undo()
        except AttributeError as e: # 这里依赖于异常处理，并不合适
            pass

if __name__ == '__main__':
    main()
```

文件创建功能使用默认文件权限来创建文件，默认文件权限具体什么样由文件系统决定。。你也许想通过向CreateFile传递恰当的参数让用户能够提供自己的权限设置。可以怎样实现呢？一种方式是通过使用 `os.fdopen()`。

异常处理不是一个程序的常规流程。

## 11.5 小结

我们学习了命令模式。使用这种设计模式，可以将一个操作（比如，复制/粘贴）封装为一个对象。这样能提供很多好处，如下所述：

* 我们可以在任何时候执行一个命令，而并不一定是在命令创建时。&#x20;
* 执行一个命令的客户端代码并不需要知道命令的任何实现细节。&#x20;
* 可以对命令进行分组，并按一定的顺序执行。

一般而言，要在运行时按照用户意愿执行的任何操作都适合使用命令模式。命令模式也适用于组合多个命令。这有助于实现宏、多级撤销以及事务。一个事务应该：要么成功，这意味着事务中所有操作应该都成功（提交操作）；要么如果至少一个操作失败，则全部失败（回滚操作）。如果希望进一步使用命令模式，可以实现一个例子，涉及将多个命令组合成一个事务。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://l1nwatch.gitbook.io/python-design-mode/di-san-bu-fen-hang-wei-xing-mo-shi/di-11-zhang-ming-ling-mo-shi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
