python 设计模式之命令模式

命令模式

1. 什么是命令模式

命令模式可将请求转换为一个包含与请求相关的所有信息的独立对象,这个独立对象实现了调用者与接收者之间的解耦,命令模式最大的杀手锏是它能非常轻松的实现撤销操作。发送者,也称触发者 命令接口,通常仅声明一个执行命令的方法

在讲解命令模式之前,我先列出命令模式中所涉及的一些核心概念:

  1. 发送者,也称触发者
  2. 命令接口,通常仅声明一个执行命令的方法
  3. 具体命令,实现各种类型的请求,但是它并不实现业务逻辑,而是委派给接收者
  4. 接收者,真正干活的,实现业务逻辑
  5. 客户端,创建并配置具体命令对象,并让命令对象与发送者关联

学习设计模式最大的困难在于我们极难找到一个合适的业务场景集合实际情况讲解,其次,人们总喜欢用自己习惯的方法解决问题,对设计模式总是提出各种质疑。

本文从一个很贴近生活的例子为你逐步拆解命令模式。

1. 设计一个文本编辑器

1.1 三种创建文件途径

我现在想设计一个文本编辑器,目前第一步,我要实现创建新文件这个功能,我打算提供3种创建文件的途径:

  1. 通过菜单创建
  2. 通过按钮创建
  3. 通过快捷键创建

3种创建途径,本质上都是在执行同一个动作,创建文件。显然,不管这3个途径的代码在哪里去编写,我只写一个创建文件的函数create_file,让他们3个去调用。

def create_file(filename):
    with open(filename, 'w')as f:
        pass

就算你不用任何设计模式,也至少能想得到代码复用,单一职能原则。

2. 命令接口

从第一节的例子开始,我逐步用命令模式来实现编辑器的创建文件功能,首先,我要定义一个命令接口

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

命令接口是所有具体命令的约束条件,具体命令必须实现execute方法来完成具体的命令,你需要注意execute方法没有任何参数,那么完成这个命令所需要的那些参数是如何获取的呢?

答案是必须通过初始化参数传递具体命令,这些参数包括了具体命令完成命令时所需要的一切资源,信息。

但是,具体命令里,我们也并不实现业务逻辑,业务逻辑由接收者来实现

3. 接收者

接收者才是那个真正干活的人

class ABCReceiver(ABC):
    @abstractmethod
    def create_file(self, filename):
        pass


class NewFileReceiver(ABCReceiver):
    def create_file(self, filename):
        with open(filename, 'w')as f:
            pass

接收者构筑了命令模式的最底层,是真正的业务实现层,在它之上是具体命令层。

4. 具体命令

现在,我们来实现具体的命令

class NewFileCommand(AbcCommand):
    def __init__(self, reciver, new_file_name):
        self.reciver = reciver
        self.new_file_name = new_file_name

    def execute(self):
        self.reciver.create_file(self.new_file_name)

NewFileCommand 是AbcCommand的子类,它的初始化参数里包括了具体的业务接收者和完成这个命令的最关键的参数,新文件的名字。

这里必须解释一下,为什么不在NewFileCommand里实现新建文件的业务逻辑,而是非要引入reciver,让reciver 去完成呢,有必要搞的这么麻烦么?

这样做,主要是为了解耦,为了分层,为了应对变化,目前实现的新建文件的功能,是在本地电脑上新建,如果将来出现一个新的功能,在云端新建文件,那么我们就可以实现一个CloudNewFileReceiver

class CloudNewFileReceiver(ABCReceiver):
    def create_file(self, filename):
        pass

我们将CloudNewFileReceiver 的实例对象传入NewFileCommand的初始化函数里,就能够实现在云端新建文件的功能,对于NewFileCommand而言,它并不关心传入的receivcer 是谁,只要这个receivcer 有create_file方法就行了。

在本地新建文件或者在云端新建文件,是由接收者这一层实现的,具体命令可以不针对新建方式在区分出本地新建命令和云端新建命令,统一的使用NewFileCommand。如果你创建一个NewCloudFileCommand 也是可行的,设计模式不是教条主义,没有严格的限定你不能如何,设计模式是经验的总结,不是放之四海而皆准的真理。

那么问题来了,谁决定NewFileCommand在初始化的时候reciver 传哪一个呢?答案是客户端

5. 客户端与发送者

前面4个小节,已经将命令模式的结构和功能描述清楚了,客户端这个概念你可以理解为命令模式运行的环境,总要有一个地方让你创建具体命令和接收者吧,它可能就是一个函数,负责编辑器的初始化。

对于客户端,我们难以指出一个具体的对象并将其称之为客户端,我们只需要牢记它的作用就好了:

客户端创建并配置具体命令对象,并让命令对象与发送者相关联。

什么又是发送者呢?发送者又成触发者,是具体命令的调用者,前面讲了新建文件的三种途径,这3个途径不就是新建文件这个动作的触发者么?

class NewFileInvoker:
    def __init__(self, command):
        self.command = command

    def run(self):
        self.command.execute()


class NewFileButtionInvoker(NewFileInvoker):
    pass

class NewFileMenuInvoker(NewFileInvoker):
    pass

class NewFileKeyboard(NewFileInvoker):
    pass

最后完成客户端代码

def init_client():
    new_file_receiver = NewFileReceiver()
    command = NewFileCommand(new_file_receiver, 'data.txt')
    buttion_invoker = NewFileButtionInvoker(command)
    buttion_invoker.run()

if __name__ == '__main__':
    init_client()

在客户端,创建了具体命令,并将命令与发送者关联到了一起,这样就实现了发送者与接收者的的解耦,这一次,我将NewFileCommand与NewFileButtionInvoker关联,用的receiver是NewFileReceiver, 下一次条件发生改变,我就可以用CloudNewFileReceiver ,就会在云端创建文件。

6. 杀手锏,撤销操作

撤销操作是命令模式的杀手锏,假设新建文件后,我想撤销这个动作,那该怎么办呢,对代码稍作修改

import os
from abc import ABC, abstractmethod


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

    @abstractmethod
    def cancel(self):
        pass

class ABCReceiver(ABC):
    @abstractmethod
    def create_file(self, filename):
        pass

    def del_file(self, filename):
        os.remove(filename)

class NewFileReceiver(ABCReceiver):
    def create_file(self, filename):
        with open(filename, 'w')as f:
            pass


class CloudNewFileReceiver(ABCReceiver):
    def create_file(self, filename):
        pass


class NewFileCommand(AbcCommand):
    def __init__(self, reciver, new_file_name):
        self.reciver = reciver
        self.new_file_name = new_file_name

    def execute(self):
        self.reciver.create_file(self.new_file_name)

    def cancel(self):
        self.reciver.del_file(self.new_file_name)


class NewFileInvoker:
    def __init__(self, command):
        self.command = command

    def run(self):
        self.command.execute()

    def cannel(self):
        self.command.cancel()

class NewFileButtionInvoker(NewFileInvoker):
    pass

class NewFileMenuInvoker(NewFileInvoker):
    pass

class NewFileKeyboard(NewFileInvoker):
    pass


def init_client():
    new_file_receiver = NewFileReceiver()
    command = NewFileCommand(new_file_receiver, 'data.txt')
    buttion_invoker = NewFileButtionInvoker(command)
    buttion_invoker.run()
    # 现在又想撤销
    buttion_invoker.cannel()

if __name__ == '__main__':
    init_client()

在AbcCommand 增加一个撤销的方法,触发者,接收者同理增加撤销的方法和删除文件的方法,这样就能够轻松的实现撤销方法,在NewFileCommand具体命令中,包含了实现这个命令所需要的一切信息,自然也就能够轻易的实现撤销操作。

除了新建操作,还有重命名操作,删除操作,修改操作,每一种操作都实现一个具体命令对象,每一个命令对象都有execute方法和cancel方法。

当你对编辑器执行了一系列操作后,想要回退,那么只需要将之前执行过的命令保存到一个列表中,在撤销回退时逆向调用每一个具体命令的cancel方法即可。

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案