前言
GOF设计模式分三大类:
- 创建型模式:关注对象的创建过程,包括单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式和建造者模式。
- 结构型模式:关注类和对象之间的组合,包括适配器模式、桥接模式、组合模式、装饰模式、外观模式、享元模式和代理模式。
- 行为型模式:关注对象之间的交互,包括职责链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式和访问者模式。
一、命令模式
命令模式(Command Pattern)
- 定义:
- 将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化
- 对请求排队或者记录请求日志
- 支持可撤销的操作
- 解决问题:如何将请求的发送者和请求的接收者完全解耦?
- 使用场景:
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。
- 系统需要将一组操作组合在一起形成宏命令。
- 组成:
- Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。
- ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法。它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。
- Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。
- Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。
- 补充说明:
- 在软件开发中也存在很多与开关和电器类似的请求发送者和接收者对象。发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。
- 在命令模式中,发送者与接收者之间引入了新的命令对象,将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。
- 命令模式的核心在于引入了命令类。请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。
- 通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法。
- 命令模式的本质是对请求进行封装。一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。
- 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
- 命令模式是一种使用频率非常高的设计模式。
- 优点:
- 降低系统的耦合度。新的命令可以很容易地加入系统中。可以比较容易地设计一个命令队列或宏命令(组合命令)。为请求的撤销(Undo)和恢复(Redo)操作提供了一种设计和实现方案。
- 缺点:
- 可能会导致某些系统有过多的具体命令类。
二、命令模式示例
使用命令模式来设计“自定义功能键”模块,将功能键和相应功能自由绑定
- FunctionButton充当请求调用者,Command充当抽象命令类,MinimizeCommand和HelpCommand充当具体命令类,WindowHandler和HelpHandler充当请求接收者。
- FBSettingWindow是“功能键设置”界面类
### 命令模式
"""请求发送者"""
class FunctionButton:
def __init__(self, name: str):
self.name = name # 功能键名称
self.cmd: Cmd = None # 抽象命令对象的引用
def on_click(self):
# 发送请求
print("点击功能键:", end="")
self.cmd.execute()
"""请求接收者"""
class WindowHandler:
# 窗口处理
def minimize(self):
print("将窗口最小化到托盘!")
class HelpHandler:
# 帮助文档处理
def display(self):
print("显示帮助文档!")
"""抽象命令类"""
class Cmd:
def execute(self):
raise NotImplementedError
"""具体命令类"""
class MinimizeCmd(Cmd):
# 最小化命令
def __init__(self):
self.wh_obj = WindowHandler()
def execute(self):
# 命令执行:调用请求接收者的业务方法
self.wh_obj.minimize()
class HelpCmd(Cmd):
# 帮助命令
def __init__(self):
self.hh_obj = HelpHandler() # 请求接收者的引用
def execute(self):
# 命令执行:调用请求接收者的业务方法
self.hh_obj.display()
"""功能键设置界面"""
class FBSettingWindow:
def __init__(self, title: str):
self.title = title # 窗口标题
self.function_buttons: list[FunctionButton] = [] # 存储所有功能键
def add_function_button(self, fb: FunctionButton):
if fb not in self.function_buttons:
self.function_buttons.append(fb)
def remove_function_button(self, fb: FunctionButton):
if fb in self.function_buttons:
self.function_buttons.remove(fb)
def display(self):
# 显示窗口及功能键
print(f"显示窗口:{self.title}")
print("显示功能键:")
for i in self.function_buttons:
print(i.name)
print("#" * 10)
- 为了提高系统的灵活性和可扩展性,这里将具体命令类的类名存储在配置文件command_conf.json中,并通过工具类XMLUtil来读取配置文件并反射生成对象。JsonUtil类的代码如下:
# 模块 utils.py
from pathlib import Path
import json
class JsonUtil:
@staticmethod
def get_value(key: str, conf_file="config.json"):
"""读取配置文件,返回配置文件中的配置"""
path = Path(conf_file)
contents = path.read_text(encoding="utf-8")
conf = json.loads(contents)
return conf.get(key, None)
- 配置文件command_conf.json中存储了具体命令类的类名,代码如下:
{
"class_name_1": "HelpCmd",
"class_name_2": "MinimizeCmd"
}
- 客户端代码
"""客户端代码"""
from command import FBSettingWindow, FunctionButton, Cmd
from utils import JsonUtil
import command
# 通过读取配置文件和反射生成具体命令对象
class_name_1 = JsonUtil.get_value("class_name_1", "command_conf.json")
class_name_2 = JsonUtil.get_value("class_name_2", "command_conf.json")
if class_name_1 is None or class_name_2 is None:
raise ValueError
cmd1: Cmd = getattr(command, class_name_1)()
cmd2: Cmd = getattr(command, class_name_2)()
# 创建功能键并绑定功能
fb1 = FunctionButton("功能键1")
fb2 = FunctionButton("功能键2")
fb1.cmd = cmd1
fb2.cmd = cmd2
# 调用功能键的业务方法
fb1.on_click()
fb2.on_click()
# 显示窗口及功能键
fbsw = FBSettingWindow("功能键设置")
fbsw.add_function_button(fb1)
fbsw.add_function_button(fb2)
fbsw.display()
- 输出结果
点击功能键:显示帮助文档!
点击功能键:将窗口最小化到托盘!
显示窗口:功能键设置
显示功能键:
功能键1
功能键2
##########
- 如果需要修改功能键的功能,例如某个功能键可以实现“自动截屏”,只需要对应增加一个新的具体命令类。在该命令类与屏幕处理者(ScreenHandler)之间创建一个关联关系,然后将该具体命令类的对象通过配置文件注入某个功能键即可,原有代码无须修改,符合开闭原则。
您正在阅读的是《设计模式Python版》专栏!关注不迷路~