设计模式 with Python 6:命令模式

设计模式 with Python 6:命令模式

命令模式

命令模式是一种将命令的调用方和接收(实现)方解耦的设计模式。

如果不明白这句话的意思,不用担心,看完这篇文章就能理解了。

智能家居APP

假设我们需要开发一款智能家居客户端,我们需要接入不同厂家不同标准的只能家居产品,虽然这些产品都提供了SDK,但具体的SDK调用方式并不相同,比如:

image-20210628173421580

用过小米台灯的应该知道,这款台灯有多个级别的亮度可以调节,而普通的台灯可能只能设置开和关,现在我们的APP需要同时接入这两款台灯,假设我们的控制面板上将接入的智能家居以一个个方块状控件显示,图标和名称表示是哪个智能家居产品,点击控件就可以简单的开启或关闭该只能家居,更详细的操作可以使用长按进入详细控制面板,简单起见我们先不考虑这种操作,只实现简单的控制面板以及具体只能家居的开关。

最先也最容易想到的设计应该是这样的:

image-20210628180223889

代码这里就不展示了,具体见Github仓库的smart_home_v1

这个方案目前似乎没有什么问题,如果需要引入新的智能家居设备,只要创建新的ControlButton子类即可,对具体智能家居设备的SDK的调用逻辑会封装在新建的子类中。

这里实际上是采用了依赖倒置的设计原则。

但是从系统抽象方面思考存在一定问题,因为ControlButton及其子类明显是属于系统的UI部分,理论上是不应该和业务逻辑部分紧耦合的。

如果我们的系统保持现在这般简单,也没有太大问题,单如果复杂一点,比如说创建一个顶部浮动菜单,可以进行对于某些常用智能家居进行快速关闭和打开,或者根本就是整合进语音助手,让语音助手可以通过语言关闭和打开具体的设备。在这些情况下让另一个UI或者干脆是语音应用再通过ControlButton来进行调用显然是不合适的,因为那些控件根本用不到image等属性,况且我们上边的设计中off()on()根本就是受保护的。

像这种需要将命令的调用方(在这里是UI控件或者语音助手)与执行、接收方(这里指智能家居的SDK组件)进行解耦的设计,正是本文要介绍的设计模式:命令模式

定义

我们先来看标准的命令模式的UML图:

图中的Client表示命令的调用方,Receiver表示命令的接收方,Command表示命令的抽象基类,ConcretCommand1ConcretCommand2表示具体的命令。

通过上面这种结构我们可以将命令的调用方与接收方进行解耦,这其中的要点在于我们将具体的命令执行逻辑和对具体命令接收方的引用都封装在了Command及其子类中,并对外以统一的接口(这里表现为execute()抽象方法`)的方式提供服务。而调用方不需要再与命令的接收方紧耦合,它只需要持有相关命令即可进行调用。

此外需要注意的是命令一般会持有一个具体接收方的引用,以实现具体的命令执行逻辑,就像这里Commandreceiver属性一般,但是在实际实现中可能命令的接收方并不会具有一个共有的基类或接口,如果那样则必然不必要也不能在命令的基类中持有Receiver引用。

最后要说明的是,命令的创建方在这个模式中并不重要,我们可以在任何地方进行创建并关联命令接收方,然后只需要在适当的时候将相关命令传递给相关的调用方即可。

实现

image-20210629110846833

这里删除了ControlButton的子类,并且ControlButton也不再是抽象类。这是因为我们已经将具体的只能家居调用逻辑封装在了命令中,不需要再使用子类化ControlButton的方式。

我们将小米台灯的开、关以及普通台灯的开、关封装为了四个命令,这是一般性的做法,当然你也可以封装为两个,每个命令具有开关两个方法,但是“粗粒度”的抽象自然会伴随较差的灵活性,你无法在其它地方随意的调用单个只需要开的命令,或者将命令组合。

这里没有绘制语音助手,因为在实现了命令模式的相关类后实现语音助手其实很简单,在具体编码时候实现即可。

同样,代码实现没有太大难度,这里就不作展示了,具体代码见Github仓库smart_home_v2

事实上我们上面做出的改进在这个具体示例中有点“用牛刀杀鸡”的感觉,毕竟就算是小米台灯也只是调整亮度到固定度数就能简单实现具体的命令要求,业务逻辑并不复杂,但是像上面这样实现了命令模式后会有一些扩展性带来的其它好处,比如我们可以轻松给语音助手添加一个类似于“撤销上一个操作”这样的功能。

撤销命令

实现起来很简单,先给抽象基类Command添加一个undo方法用于实现撤销逻辑:

from abc import ABC, abstractmethod


class Command(ABC):
    @abstractmethod
    def execute(self):
        pass
    @abstractmethod
    def undo(self):
        pass

自然的,我们需要给Command子类全部添加上undo方法:

from smart_home_v3.src.MiDeskLamp import MiDeskLamp
from smart_home_v3.src.commnad.Command import Command


class MiDeskLampOffCommand(Command):
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值