第 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 实现

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

  • 创建一个文件,并随意写入一个字符串

  • 读取一个文件的内容

  • 重命名一个文件

  • 删除一个文件

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

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 小结

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

  • 我们可以在任何时候执行一个命令,而并不一定是在命令创建时。

  • 执行一个命令的客户端代码并不需要知道命令的任何实现细节。

  • 可以对命令进行分组,并按一定的顺序执行。

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

Last updated