命令模式(The Command Pattern):用来给应用添加Undo操作,将命令操作封装为对象,控制命令的执行时间和过程。
命令模式是一种行为设计模式,它可将请求转换为一个包含与请求相关的所有信息的独立对象。该转换让你能根据不同的请求将方法参数化、延迟请求执行或将其放入队列中,且能实现可撤销操作。
命令模式帮助我们把一个操作(undo,redo,copy,paste等)封装成一个对象,通常是创建一个包含Operation所有逻辑和方法的类。 通过命令模式可以控制命令的执行时间和过程,还可以用来组织事务。
1 介绍
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 类 3、invoker 使用命令对象的入口
优点:
- 单一职责原则。你可以解耦触发和执行操作的类。
- 开闭原则。你可以在不修改已有客户端代码的情况下在程序中创建新的命令。
- 你可以实现撤销和恢复功能。
- 你可以实现操作的延迟执行。
- 你可以将一组简单命令组合成一个复杂命令。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 代码可能会变得更加复杂,因为你在发送者和接收者之间增加了一个全新的层次。
2 适用场景
命令在发送方被激活,而在接收方被响应。一个对象既可以作为命令的发送方,也可以作为命令的接收方,或者都可以。命令的典型应用就是图形用户界面开发。每一个窗体都包含菜单,工具栏,按钮等控件,将用户的单击动作也叫命令作为外部事件,然后系统会根据绑定的事件处理程序执行相应的动作即命令获得响应,完成用户发出的请求。
大多数情况下,命令的调用者和接收者是前期绑定的,但是想要对命令进行操作的时候,就需要撤销命令的调用者和接收者之间的紧密耦合关系,使得二者相互独立。
命令模式(Command Pattern):将请求封装成对象,将其作为命令调用者和接收者的中介,而抽象出来的命令对象又使得能够对一系列请求进行特殊操作,比如:对请求排队或记录请求日志,以及支持可撤消的操作.
常见适用场景如下:
- 认为是命令的地方都可以使用命令模式
比如: GUI 中每一个按钮都是一条命令;模拟 CMD
- 如果你需要通过操作来参数化对象,可使用命令模式
命令模式可将特定的方法调用转化为独立对象。这一改变也带来了许多有趣的应用:你可以将命令作为方法的参数进行传递、将命令保存在其他对象中,或者在运行时切换已连接的命令等。
举个例子:你正在开发一个 GUI 组件(例如上下文菜单),你希望用户能够配置菜单项,并在点击菜单项时触发操作。
- 如果你想要将操作放入队列中、操作的执行或者远程执行操作,可使用命令模式。
同其他对象一样,命令也可以实现序列化(序列化的意思是转化为字符串),从而能方便地写入文件或数据库中。一段时间后,该字符串可被恢复成为最初的命令对象。因此,你可以延迟或计划命令的执行。但其功能远不止如此!使用同样的方式,你还可以将命令放入队列、记录命令或者通过网络发送命令。
- 如果你想要实现操作回滚功能,可使用命令模式。
尽管有很多方法可以实现撤销和恢复功能,但命令模式可能是其中最常用的一种。
为了能够回滚操作,你需要实现已执行操作的历史记录功能。命令历史记录是一种包含所有已执行命令对象及其相关程序状态备份的栈结构。
这种方法有两个缺点。首先,程序状态的保存功能并不容易实现,因为部分状态可能是私有的。你可以使用备忘录模式来在一定程度上解决这个问题。
其次,备份状态可能会占用大量内存。因此,有时你需要借助另一种实现方式:命令无需恢复原始状态,而是执行反向操作。反向操作也有代价:它可能很难实现,甚至无法实现。
3 使用步骤
命令模式常用代码结构如下:
-
声明仅有一个执行方法的命令接口。
-
抽取请求并使之成为实现命令接口的具体命令类。每个类都必须有一组成员变量来保存请求参数和对于实际接收者对象的引用。所有这些变量的数值都必须通过命令构造函数进行初始化。
-
找到担任发送者职责的类。在这些类中添加保存命令的成员变量。发送者只能通过命令接口与其命令进行交互。发送者自身通常并不创建命令对象,而是通过客户端代码获取。
-
修改发送者使其执行命令,而非直接将请求发送给接收者。
-
客户端必须按照以下顺序来初始化对象:
- 创建接收者。
- 创建命令,如有需要可将其关联至接收者。
- 创建发送者并将其与特定命令关联。
通用代码结构示例如下:
from abc import ABC, abstractmethod
"""
步骤1:定义请求发送者invoker,负责对请求进行初始化。
其中必须包含一个成员变量来存储对于命令对象的引用。发送者触发命令,而不向接收者直接发送请求。
注意,发送者并不负责创建命令对象:它通常会通过构造函数从客户端处获得预先生成的命令。
"""
class Invoker(object):
_on_start = None
_on_finish = None
command_list = []
"""初始化命令"""
def set_on_start(self, command):
self._on_start = command
def set_on_finish(self, command):
self._on_finish = command
def set_command(self, command):
self.command_list.append(command)
def do_something_important(self):
"""
请求发送者不依赖于具体的命令或者receiver类。Invoker通过执行一个command,间接的将请求发送出去
"""
print("Invoker: 任务开始之前执行前置命令")
if isinstance(self._on_start, Command):
self._on_start.execute()
print("Invoker: 开始执行指令")
for cmd in self.command_list:
cmd.execute()
print("Invoker: ...all task done...")
print("Invoker: 任务完成之前后执行后置命令")
if isinstance(self._on_finish, Command):
self._on_finish.execute()
"""步骤2: 申明执行命令的方法"""
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
"""步骤3: 具体命令,实现各种类型的请求"""
class BasicCommand(Command):
def __init__(self, payload):
self._payload = payload
def execute(self) -> None:
print(f'无receiver对象,执行简单命令: {self._payload}')
"""
步骤3: 具体命令,接收对象执行方法所需的参数,会实现各种类型的请求。
具体命令自身并不完成工作,而是会将调用委派给一个业务逻辑对象。但为了简化代码,这些类可以进行合并。
接收对象执行方法所需的参数可以声明为具体命令的成员变量。你可以将命令对象设为不可变,仅允许通过构造函数对这些成员变量进行初始化。
"""
class ComplexCommand(Command):
"""将Invoker和Receiver关联"""
def __init__(self, receiver, a: str, b: str) -> None:
self._receiver = receiver
self._a = a
self._b = b
def execute(self) -> None:
print(f'ComplexCommand: 关联了receiver对象,执行命令')
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)
"""
步骤4:定义接收者receiver。
接收者(Receiver)类包含部分业务逻辑。几乎任何对象都可以作为接收者。
绝大部分命令只处理如何将请求传递到接收者的细节,接收者自己会完成实际的工作
"""
class Reveiver(object):
"""
定义主要的业务逻辑。处理来自Invoker的各种命令对应请求
"""
def do_something(self, param_a):
print(f"Receiver执行task1 with {param_a}")
def do_something_else(self, param_b):
print(f'Receiver执行task2 with {param_b}')
"""
步骤5:创建并配置具体的命令对象。
客户端必须将包括接收者实体在内的所有请求参数传递给命令的构造函数。此后,生成的命令就可以与一个或多个发送者相关联了。
"""
def main():
invoker = Invoker() #发送者
invoker.set_on_start(BasicCommand("执行基本命令")) #发送简单的命令
receiver = Reveiver() #接收者
invoker.set_command(ComplexCommand(receiver, "a: param", "b: param"))#Invoker发送命令给Receiver
invoker.set_on_finish(ComplexCommand(receiver, "a: send mail", "b: send message"))
invoker.do_something_important()
if __name__ == "__main__":
main()
执行结果:
Invoker: 任务开始之前执行前置命令
无receiver对象,执行简单命令: 执行基本命令
Invoker: 开始执行指令
ComplexCommand: 关联了receiver对象,执行命令
Receiver执行task1 with a: param
Receiver执行task2 with b: param
Invoker: ...all task done...
Invoker: 任务完成之前后执行后置命令
ComplexCommand: 关联了receiver对象,执行命令
Receiver执行task1 with a: send mail
Receiver执行task2 with b: send message
4 示例代码
案例1: 使用命令模式实现最基本的文件操作工具: 1,创建一个文件,并随意写入一个字符串 2,读取一个文件的内容 3,重命名一个文件 4,删除一个文件
代码:
import os
from abc import ABC, abstractmethod
verbose = True
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class RenameFile(Command):
def __init__(self,src, dest):
self._src = src
self._dest = dest
def execute(self):
if verbose:
print(f"rename {self._src} with name {self._dest}")
os.rename(self._src, self._dest)
def undo(self):
if verbose:
print(f"rename back from {self._dest} with name {self._src}")
os.rename(self._dest, self._src)
class CreateFile(Command):
def __init__(self, file_path, data="Hello python"):
self._file_path, self._data = file_path, data
def execute(self):
if verbose:
print(f"create file: {self._file_path} with data {self._data}")
with open(self._file_path, 'w') as f:
f.write(self._data)
def undo(self):
if verbose:
print(f"delete file: {self._file_path}")
os.remove(self._file_path)
class DeleteFile(Command):
def __init__(self, file_path):
self._file_path = file_path
self._tmp_path = self._file_path + ".tmp"
def execute(self):
if verbose:
print(f"delete file: {self._file_path}")
os.rename(self._file_path, self._tmp_path)
def undo(self):
if verbose:
print(f"recover file: {self._file_path}")
os.rename(self._tmp_path, self._file_path)
class Invoker(object):
command_list = []
def register_cmd(self, command):
self.command_list.append(command)
def cancel_cmd(self, command):
self.command_list.remove(command)
def run_cmd(self):
for cmd in self.command_list:
cmd.execute()
def recovery(self):
for cmd in self.command_list:
cmd.undo()
def main():
org_file = "test.txt"
new_file = "demo.txt"
invoker = Invoker()
invoker.register_cmd(CreateFile(org_file, u"人生苦短,我用python"))
invoker.register_cmd(RenameFile(org_file, new_file))
invoker.register_cmd(DeleteFile(new_file))
invoker.run_cmd()
invoker.command_list = reversed(invoker.command_list)
invoker.recovery()
if __name__ == "__main__":
main()
执行结果:
create file: test.txt with data 人生苦短,我用python
rename test.txt with name demo.txt
delete file: demo.txt
recover file: demo.txt
rename back from demo.txt with name test.txt
delete file: test.txt
案例2: 在大多数饭店中,当服务员已经接到顾客的点单,录入到系统中后,根据不同的菜品,会有不同的后台反应。 比如,饭店有凉菜间、热菜间、主食间,那当服务员将菜品录入到系统中后,凉菜间会打印出顾客所点的凉菜条目,热菜间会打印出顾客所点的热菜条目,主食间会打印出主食条目。 那这个系统的后台模式该如何设计?
参考代码:
from abc import ABC, abstractmethod
"""步骤1:定义receiver,即主食子系统,凉菜子系统,热菜子系统,后台三个子系统"""
class BackSys(ABC):
def cook(self):
pass
class MainFoodSys(BackSys):
def __init__(self, dish):
self._dish = dish
def cook(self):
print(f"主食cook : { self._dish}")
class HotFoodSys(BackSys):
def __init__(self, dish):
self._dish = dish
def cook(self):
print(f"热食cook : {self._dish}")
class ColdFoodSys(BackSys):
def __init__(self, dish):
self._dish = dish
def cook(self):
print(f"冷食cook : {self._dish}")
"""
步骤2:定义Invoker类,服务员点单系统
"""
class WaiterInvoker(object):
order_list = []
def add_order(self, cmd):
print(f"添加订单:{cmd.dish}")
self.order_list.append(cmd)
def cancel_order(self, cmd):
print(f"取消订单:{cmd.dish}")
self.order_list.remove(cmd)
def confirm_order(self):
print(f"订单提交")
for od in self.order_list:
od.execute()
"""
步骤3:定义command类,执行命令
"""
class Command(ABC):
receiver = None
@abstractmethod
def execute(self):
pass
class foodCommand(Command):
dish = ""
def __init__(self,receiver,dish):
self.receiver=receiver
self.dish=dish
def execute(self):
self.receiver.cook()
class mainFoodCommand(foodCommand):
pass
class coolDishCommand(foodCommand):
pass
class hotDishCommand(foodCommand):
pass
"""
菜单类辅助业务
"""
class Menu(object):
menu_map = dict()
def loadMenu(self): # 加载菜单,这里直接写死
self.menu_map["hot"] = ["Yu-Shiang Shredded Pork", "Sauteed Tofu, Home Style", "Sauteed Snow Peas"]
self.menu_map["cool"] = ["Cucumber", "Preserved egg"]
self.menu_map["main"] = ["Rice", "Pie"]
def isHot(self, dish):
if dish in self.menu_map["hot"]:
return True
return False
def isCool(self, dish):
if dish in self.menu_map["cool"]:
return True
return False
def isMain(self, dish):
if dish in self.menu_map["main"]:
return True
return False
def main():
dish_list = ["Yu-Shiang Shredded Pork", "Sauteed Tofu, Home Style", "Cucumber", "Rice"] # 顾客点的菜
waiter_sys = WaiterInvoker()
menu = Menu()
menu.loadMenu()
for dish in dish_list:
main_food_sys = MainFoodSys(dish)
cool_dish_sys = ColdFoodSys(dish)
hot_dish_sys = HotFoodSys(dish)
if menu.isCool(dish):
cmd = coolDishCommand(cool_dish_sys, dish)
elif menu.isHot(dish):
cmd = hotDishCommand(hot_dish_sys, dish)
elif menu.isMain(dish):
cmd = mainFoodCommand(main_food_sys, dish)
else:
continue
waiter_sys.add_order(cmd)
waiter_sys.confirm_order()
if __name__ == "__main__":
main()
运行结果:
添加订单:Yu-Shiang Shredded Pork
添加订单:Sauteed Tofu, Home Style
添加订单:Cucumber
添加订单:Rice
订单提交
热食cook : Yu-Shiang Shredded Pork
热食cook : Sauteed Tofu, Home Style
冷食cook : Cucumber
主食cook : Rice
5 与其他模式关系
职责链、命令、中介者和观察者用于处理请求发送者和接收者之间的不同连接方式:
- 职责链按照顺序将请求动态传递给一系列的潜在接收者,直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接,强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
-
职责链的管理者可以使用命令模式来实现。在这种情况下,你可以对由请求代表的同一个上下文对象执行许多不同的操作。
还有另外一种实现方式,那就是请求自身就是一个命令对象。在这种情况下,你可以对由一系列不同上下文连接而成的链执行相同的操作。
-
你可以同时使用命令和备忘录来实现“撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
命令和策略看上去很像,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同。
-
你可以使用命令来将任何操作转换为对象。操作的参数将成为对象的成员变量。你可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。
-
另一方面,策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
你可以将访问者视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。
参考文献:
https://refactoringguru.cn/design-patterns/command
https://www.cnblogs.com/welan/p/9130726.html