liusarazhang 2020-07-04
现在多数应用都有撤销操作。虽然难以想象,但在很多年里,任何软件中确实都不存在撤销操作。撤销操作是在1974年引入的,但Fortran和Lisp分别早在1957年和1958年就已创建了撤销操作。
命令设计模式帮助我们将一个操作(撤销、重做、复制、粘贴等)封装成一个对象。简而言之,这意味着创建一个类,包含实现该操作所需要的所有逻辑和方法。这样做的优势如下所述:
1. 现实生活中的例子
当我们去餐馆吃饭时,会叫服务员来点单。他们用来做记录的账单(通常是纸质的)就是命令模式的一个例子。在记录好订单后,服务员将其放入账单队列,厨师会照着单子去做。每个账单都是独立的,并且可用来执行许多不同命令,例如,一个命令对应一个将要烹饪的菜品。
2. 软件的例子
PyQt是QT工具包的Python绑定。 PyQt包含一个QAction类,将一个动作建模为一个命令。对每个动作都支持额外的可选信息,比如,描述、工具提示、快捷键和其他。
3. 应用案例
许多开发人员以为撤销例子是命令模式的唯一应用案例。撤销操作确实是命令模式的杀手级特性,然而命令模式能做的实际上还有很多。
4. 实现
? 我们将使用命令模式实现最基本的文件操作工具。
我们并不从头实现这些工具程序,因为Python在os模块中已提供了良好的实现。我们想做的是在已有实现之上添加一个额外的抽象层,这样可以当作命令来使用。这样,我们就能获得命令提供的所有优势。
下面的用例图展示了实现将支持的用户可执行操作。从展示的操作可以看出,重命名文件和创建文件支持撤销。删除一个文件和读取文件内容不支持撤销。对于文件删除操作实际上是可以实现撤销的,一种技术是使用一个特殊的垃圾箱/废物篓目录来存储所有被删除文件,这样在用户请求时可以恢复出来。
每个命令都包括两个部分,初始化部分和执行部分。初始化部分由__init__()方法完成,包含该命令发挥作用所要求的所有信息(文件路径和将写入文件的内容等)。执行部分由execute()方法完成。在我们想真正地运行命令时才调用其execute()方法。该方法并不需要在命令初始化之后立即调用。
我们从重命名工具开始,使用RenameFile类来实现。 init()方法接受源文件路径( path_src)和目标文件路径( path_dest)作为参数。如果文件路径未使用路径分隔符,则在当前目录下创建文件。使用路径分隔符的一个例子是传递字符串/tmp/file1作为path_src,字符串/home/user/file2作为path_dest。不使用路径的例子则是传递file1作为path_src,file2作为path_dest。
class RenameFile: def __init__(self, path_src, path_dest): self.src, self.dest = path_src, path_dest
execute()方法使用os.rename()完成实际的重命名。 verbose是一个全局标记,被激活时(默认是激活的),能向用户反馈执行的操作。如果你倾向于静默地执行命令,则可以取消激活状态。注意,虽然对于示例来说print()足够好了,但通常会使用更成熟更强大的方式,例如,日志模块。
def execute(self): if verbose: print("[renaming ‘{}‘ to ‘{}‘]".format(self.src, self.dest)) os.rename(self.src, self.dest)
我们的重命名工具通过undo()方法支持撤销操作。在这里,撤销操作再次使用os.rename()将文件名恢复为原始值。
def undo(self): if verbose: print("[renaming ‘{}‘ back to ‘{}‘]".format(self.dest, self.src)) os.rename(self.dest, self.src)
文件删除功能实现为单个函数,而不是一个类。我想让你明白并不一定要为想要添加的每个命令(之后会涉及更多)都创建一个新类。 delete_file()函数接受一个字符串类型的文件路径,并使用os.remove()来删除它。
def delete_file(path): if verbose: print("deleting file ‘{}‘".format(path)) os.remove(path)
再次回到使用类的方式。 CreateFile类用于创建一个文件。 init()函数接受熟悉的path参数和一个txt字符串,默认向文件写入hello world文本。通常来说,合理的默认行为是创建一个空文件,但因这个例子的需要,我决定向文件写个一个默认字符串。可以根据需要更改它。
def __init__(self, path, txt=‘hello world\n‘): self.path, self.txt = path, txt
execute()方法使用with语句和open()来打开文件( mode=‘w‘意味着写模式),并使用write()来写入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)
创建一个文件的撤销操作是删除它。因此, undo()简单地使用delete_file()来实现目的。
def undo(self): delete_file(self.path)
最后一个工具让我们能够读取文件内容。 ReadFile类的execute()方法再次使用with()语句配合open(),这次是读模式,并且只是使用print()来输出文件内容。
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=‘‘)
main()函数使用这些工具类/方法。参数orig_name和new_name是待创建文件的原始名称以及重命名后的新名称。 commands列表用于添加(并配置)所有我们之后想要执行的命令。注意,命令不会被执行,除非我们显式地调用每个命令的execute()。
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]
下一步是询问用户是否需要撤销执行过的命令。用户选择撤销命令或不撤销。如果选择撤销,则执行commands列表中所有命令的undo()。然而,由于并不是所有命令都支持撤销,因此在undo()方法不存在时产生的AttributeError异常要使用异常处理来捕获。如果你不喜欢对这种 情 况 使 用 异 常 处 理 , 可 以 通 过 添 加 一 个 布 尔 方 法 ( 例 如 , supports_undo() 或can_be_undone())来显式地检测命令是否支持撤销操作。
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
以下是该示例的完整代码。
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()
输出如下。第一次不撤销命令,第二次则会撤销。
[creating file ‘file1‘] [reading file ‘file1‘] hello world [renaming ‘file1‘ to ‘file2‘] reverse the executed commands? [y/n] n the result is file2 >>> python3 command.py [creating file ‘file1‘] [reading file ‘file1‘] hello world [renaming ‘file1‘ to ‘file2‘] reverse the executed commands? [y/n] y [renaming ‘file2‘ back to ‘file1‘] deleting file ‘file1‘
5. 小结
使用这种设计模式,可以将一个操作(比如,复制/粘贴)封装为一个对象。这样能提供很多好处,如下所述。
执行一个命令就像在餐馆里点单。每个顾客的订单都是一个独立的命令,分多个阶段,最终由厨师来执行。