行为型模式:
介绍处理系统实体之间通信的设计模式。
①.责任链模式
简介:
开发一个应用时,多数时候我们都能预先知道哪个方法能处理某个特定请求。然而,情况并非总是如此。例如,想想任意一种广播计算机网络,例如早的以太网实现。在广播计算机网络中,会将所有请求发送给所有节点(简单起见,不考虑广播域),但仅对所发送请求感兴趣的节点会处理请求。加入广播网络的所有计算机使用一种常见的媒介相互连接,
.
如果一个节点对某个请求不感兴趣或者不知道如何处理这个请求,可以执行以下两个操作:
1. 忽略这个请求,什么都不做
2. 将请求转发给下一个节点
.
责任链(Chain of Responsibility)模式用于让多个对象来处理单个请求时,或者用于预先不知道应该由哪个对象(来自某个对象链)来处理某个特定请求时。其原则 如下所示:
.
1. 存在一个对象链(链表、树或任何其他便捷的数据结构)。
2. 我们一开始将请求发送给链中的第一个对象。
3. 对象决定其是否要处理该请求。
4. 对象将请求转发给下一个对象。
5. 重复该过程,直到到达链尾。
.
客户端代码仅知道第一个处理元素,而非拥有对所有处理元素的引用;并且每个处理元素仅知道其直接的下一个邻居(称为后继),而不知道所有其他处理元素。这通常是一种单向关系,用编程术语来说是一个单向链表,与之相反的是双向链表。单向链表不允许双向地遍历元素,双向链表则是允许的。这种链式组织方式大有用处:可以解耦发送方(客户端)和接收方(处理元素)
现实生活的例子:
ATM机以及及一般而言用于接收/返回钞票或硬币的任意类型机器(比如,零食自动贩卖机)都使用了责任链模式。
机器上总会有一个放置各种钞票的槽口,钞票放入之后,会被传递到恰当的容器。钞票返回时,则是从恰当的容器中获取,我们可以把这个槽口视为共享通信媒介,不同的容器则是处理元素。结果包含来自一个或多个容器的现金。
软件的例子:
我试过寻找一些使用责任链模式的Python应用的好例子,但是没找到,很可能是因为Python程序员不使用这个名称。因此,很抱歉,我将使用其他编程语言的例子作为参考。
.
Java的servlet过滤器是在一个HTTP请求到达目标处理程序之前执行的一些代码片段。在使用servlet过滤器时,有一个过滤器链,其中每个过滤器执行一个不同动作(用户身份验证、记日志、数据压缩等),并且将请求转发给下一个过滤器直到链结束;如果发生错误(例如,连续三次身 份验证失败)则跳出处理流程。
.
Apple的Cocoa和Cocoa Touch框架使用责任链来处理事件。在某个视图接收到一个其并不知道如何处理的事件时,会将事件转发给其超视图,直到有个视图能够处理这个事件或者视图链结束。
应用案例:
通过使用责任链模式,我们能让许多不同对象来处理一个特定请求。在我们预先不知道应该由哪个对象来处理某个请求时,这是有用的。其中一个例子是采购系统。在采购系统中,有许多核准权限。某个核准权限可能可以核准在一定额度之内的订单,假设为100美元。如果订单超过了100美元,则会将订单发送给链中的下一个核准权限,比如能够核准在200美元以下的订单。
.
另一个责任链可以派上用场的场景是,在我们知道可能会有多个对象都需要对同一个请求进行处理之时。这在基于事件的编程中是常有的事情。单个事件,比如一次鼠标左击,可被多个事件监听者捕获
.
不过应该注意,如果所有请求都能被单个处理程序处理,责任链就没那么有用了,除非确实不知道会是哪个程序处理请求。这一模式的价值在于解耦。客户端与所有处理程序(一个处理程序与所有其他处理程序之间也是如此)之间不再是多对多关系,客户端仅需要知道如何与链的起始节点(标头)进行通信。
代码实现:
class Event:
"""
Event类描述一个事件。为了让它简单一点,在我们的案例中一个事件只有一个name属性。
"""
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
class Widget:
"""
Widget类是应用的核心类。UML图中展示的parent聚合关系表明每个控件都有一个到父对象的引用。按照约定,
我们假定父对象是一个Widget实例。然而,注意,根据继承的规则,
任何Widget子类的实例(例如,MsgText的实例)也是Widget实例。parent的默认值为None。
"""
def __init__(self, parent=None):
self.parent = parent
def handle(self, event):
"""
handle()方法使用动态分发,通过hasattr()和getattr()决定一个特定请求(event) 应该由谁来处理。
如果被请求处理事件的控件并不支持该事件,则有两种回退机制。如果控件有 parent,则执行parent的handle()方法
如果控件没有parent,但有handle_default()方法,则执行handle_default()。
"""
handler = 'handle_{}'.format(event)
if hasattr(self, handler):
getattr(self, handler)(event)
elif self.parent:
self.parent.handle(event)
elif hasattr(self, 'handle_default'):
self.handle_default(
event)
class MainWindow(Widget):
"""
MainWindow是行为的控件。能处理close和default事件。
"""
def handle_close(self, event):
print('MainWindow: {}'.format(event))
def handle_default(self, event):
print('MainWindow Default: {}'.format(event))
class SendDialog(Widget):
"""
SendDialog是行为的控件。SendDialog仅能处理paint事件。
"""
def handle_paint(self, event): print('SendDialog: {}'.format(event))
class MsgText(Widget):
"""
MMsgText是行为的控件。MsgText仅能处理down事件
"""
def handle_down(self, event):
print('MsgText: {}'.format(event))
def main():
"""
main()函数展示如何创建一些控件和事件,以及控件如何对那些事件作出反应。所有事件都会被发送给所有控件。
注意其中每个控件的父子关系。sd对象(SendDialog的一个实例)的 父对象是mw(MainWindow的一个实例)。
然而,并不是所有对象都需要一个MainWindow实例的 父对象。例如,msg对象(MsgText的一个实例)是以sd作为父对象。
"""
mw = MainWindow()
sd = SendDialog(mw)
msg = MsgText(sd)
for e in ('down', 'paint', 'unhandled', 'close'):
evt = Event(e)
print('\nSending event -{}- to MainWindow'.format(evt))
mw.handle(evt)
print('Sending event -{}- to SendDialog'.format(evt))
sd.handle(evt)
print('Sending event -{}- to MsgText'.format(evt))
msg.handle(evt)
if __name__ == '__main__':
main()
责任链事件小结:
了责任链设计模式。在无法预先知道处理程序的数量和类型时,该模式有助于对请求或处理事件进行建模。适合使用责任链模式的系统例子包括基于事件的系统、采购系统和运输系统。
.
在责任链模式中,发送方可直接访问链中的首个节点。若首个节点不能处理请求,则转发给下一个节点,如此直到请求被某个节点处理或者整个链遍历结束。这种设计用于实现发送方与接收方(多个)之间的解耦。
.
ATM机是责任链的一个例子。用于取放钞票的槽口可看作是链的头部。从这里开始,根据具体交易,一个或多个容器会被用于处理交易。这些容器可看作是链中的处理程序。
.
Java的servlet过滤器使用责任链模式对一个HTTP请求执行不同的动作(例如,压缩和身份验证)。Apple的Cocoa框架使用相同的模式来处理事件,比如,按钮和手势。
②.命令模式
命令设计模式:
命令设计模式帮助我们将一个操作(撤销、重做、复制、粘贴等)封装成一个对象。简而言之,这意味着创建一个类,包含实现该操作所需要的所有逻辑和方法。
.
1. 我们并不需要直接执行一个命令。命令可以按照希望执行。
2. 调用命令的对象与知道如何执行命令的对象解耦。调用者无需知道命令的任何实现细节。
3. 如果有意义,可以把多个命令组织起来,这样调用者能够按顺序执行它们。例如,在实现一个多层撤销命令时,这是很有用的。
现实生活的例子:
当我们去餐馆吃饭时,会叫服务员来点单。他们用来做记录的账单(通常是纸质的)就是命令模式的一个例子。在记录好订单后,服务员将其放入账单队列,厨师会照着单子去做。每个账单都是独立的,并且可用来执行许多不同命令。
软件的例子:
PyQt是QT工具包的Python绑定。PyQt包含一个QAction类,将一个动作建模为一个命令。对每个动作都支持额外的可选信息,比如,描述、工具提示、快捷键和其他。
git-cola是使用Python语言编写的一个Git GUI,它使用命令 模式来修改模型、变更一次提交、应用一个差异选择、签出。
应用案例:
许多开发人员以为撤销例子是命令模式的唯一应用案例。撤销操作确实是命令模式的杀手级 特性,然而命令模式能做的实际上还有很多:
1. GUI按钮和菜单项:前面提过的PyQt例子使用命令模式来实现按钮和菜单项上的动作。
2. 其他操作:除了撤销,命令模式可用于实现任何操作。其中一些例子包括剪切、复制、 粘贴、重做和文本大写。
3. 事务型行为和日志记录:事务型行为和日志记录对于为变更记录一份持久化日志是很重要的。操作系统用它来从系统崩溃中恢复, 关系型数据库用它来实现事务,文件系统用 它来实现快照,而安装程序(向导程序)用它来恢复取消的安装。
4. 宏:在这里,宏是指一个动作序列,可在任意时间点按要求进行录制和执行。流行的编辑器(比如,Emacs和Vim)都支持宏。
代码实现:
import os
verbose = True
class RenameFile:
"""
重命名工具,使用RenameFile类来实现。__init__()方法接受源文件路径(path_src)和目标文件路径(path_dest)作为参数。
如果文件路径未使用路径分隔符,则在当前目录下创建文件。使用路径分隔符的一个例子是传递字符串/tmp/file1作为path_src,
字符串/home/user/file2作为path_dest。不使用路径的例子则是传递file1作为path_src, file2作为path_dest。
"""
def __init__(self, path_src, path_dest):
self.src, self.dest = path_src, path_dest
def execute(self):
"""
execute()方法使用os.rename()完成实际的重命名。verbose是一个全局标记,被激活时(默认是激活的),能向用户反馈执行的操作。
如果你倾向于静默地执行命令,则可以取消激活状态。注意,虽然对于示例来说print()足够好了,
但通常会使用更成熟更强大的方式,例如,日志模块
"""
if verbose:
print("[renaming '{}' to '{}']".format(self.src, self.dest))
os.rename(self.src, self.dest)
def undo(self):
"""
我们的重命名工具通过undo()方法支持撤销操作。在这里,撤销操作再次使用os.rename() 将文件名恢复为原始值。
:return:
"""
if verbose:
print("[renaming '{}' back to '{}']".format(self.dest, self.src))
os.rename(self.dest, self.src)
class CreateFile:
"""
再次回到使用类的方式。CreateFile类用于创建一个文件。__init__()函数接受熟悉的path参数和一个txt字符串,
默认向文件写入hello world文本。通常来说,合理的默认行为是创建一个空文件,但因这个例子的需要,
我决定向文件写个一个默认字符串。可以根据需要更改它
"""
def __init__(self, path, txt='hello world\n'):
self.path, self.txt = path, txt
def execute(self):
"""
execute()方法使用with语句和open()来打开文件(mode='w'意味着写模式),并使用 write()来写入txt字符串。
:return:
"""
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):
"""
创建一个文件的撤销操作是删除它。因此,undo()简单地使用delete_file()来实现目的。
:return:
"""
delete_file(self.path)
class ReadFile:
"""
ReadFile类的execute()方法再次使用with() 语句配合open(),这次是读模式,并且只是使用print()来输出文件内容。
"""
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):
"""
文件删除功能实现为单个函数,而不是一个类。我想让你明白并不一定要为想要添加的每个命令(之后会涉及更多)都创建一个新类。
delete_file()函数接受一个字符串类型的文件路 径,并使用os.remove()来删除它。
"""
if verbose:
print("deleting file '{}'".format(path))
os.remove(path)
def main():
# main()函数使用这些工具类/方法。参数orig_name和new_name是待创建文件的原始名称以及重命名后的新名称。
orig_name, new_name = 'file1', 'file2'
# commands列表用于添加(并配置)所有我们之后想要执行的命令。注意,命令不会被执行,除非我们显式地调用每个命令的execute()。
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()
# 如果选择撤销,则执行commands列表中所有命令的undo()。由于并不是所有命令都支持撤销,
# 因此在undo()方法不存在时产生的AttributeError异常要使用异常处理来捕获。如果你不喜欢对这种情况使用
# 异常处理,可以通过添加一个布尔方法(例如,supports_undo() 或 can_be_undone())来显式地检测命令是否支持撤销操作。
for c in reversed(commands):
try:
c.undo()
except AttributeError as e:
pass
if __name__ == "__main__":
main()
命令模式总结:
我们学习了命令模式。使用这种设计模式,可以将一个操作(比如,复制/粘贴) 封装为一个对象。这样能提供很多好处,如下所述:
.
1. 我们可以在任何时候执行一个命令,而并不一定是在命令创建时。
2. 执行一个命令的客户端代码并不需要知道命令的任何实现细节。
3. 可以对命令进行分组,并按一定的顺序执行。
.
虽然至今命令模式最广为人知的特性是撤销操作,但它还有更多用处。一般而言,要在运行时按照用户意愿执行的任何操作都适合使用命令模式。命令模式也适用于组合多个命令。这有助于实现宏、多级撤销以及事务。一个事务应该:要么成功,这意味着事务中所有操作应该都成功(提交操作);要么如果至少一个操作失败,则全部失败(回滚操作)。
③.解释器模式:
简介:
对每个应用来说,至少有以下两种不同的用户分类:
1. 基本用户:这类用户只希望能够凭直觉使用应用。他们不喜欢花太多时间配置或学习应用的内部。对他们来说,基本的用法就足够了。 2. 高级用户:这些用户,实际上通常是少数,不介意花费额外的时间学习如何使用应用的。高级特性。如果知道学会之后能得到以下好处,他们甚至会去学习一种配置(或脚本)语言。
1. 能够更好地控制一个应用
2. 以更好的方式表达想法
3. 提高生产力
.
解释器(Interpreter)模式仅能引起应用的高级用户的兴趣。这是因为解释器模式背后的主要思想是让非初级用户和领域专家使用一门简单的语言来表达想法。
.
领域特定语言(Domain Specific Language,DSL)DSL是一种针对一个特定领域的有限表达能力的计算机语言。很多不同的事情都使用DSL,比如,战斗模拟、记账、可视化、配置、通信协议等。DSL分为内部DSL和外部DSL:
.
1. 内部DSL构建在一种宿主编程语言之上。内部DSL的一个例子是,使用Python解决线性方程组的一种语言。使用内部DSL的优势是我们不必担心创建、编译及解析语法,因为这些已经被宿主语言解决掉了。劣势是会受限于宿主语言的特性。如果宿主语言不具备这些特性,构建一种表达能力强、简洁而且优美的内部DSL是富有挑战性的
.
2. 外部DSL不依赖某种宿主语言。DSL的创建者可以决定语言的方方面面(语法、句法等),但也要负责为其创建一个解析器和编译器。为一种新语言创建解析器和编译器是一个非常复杂、长期而又痛苦的过程。
.
解释器模式仅与内部DSL相关。因此,我们的目标是使用宿主语言提供的特性构建一种简单但有用的语言,在这里,宿主语言是Python。注意,解释器根本不处理语言解析,它假设我们已 经有某种便利形式的解析好的数据,可以是抽象语法树(abstract syntax tree,AST)或任何其他好用的数据结构。
现实生活的例子:
音乐演奏者是现实中解释器模式的一个例子。五线谱图形化地表现了声音的音调和持续时间。音乐演奏者能根据五线谱的符号精确地重现声音。在某种意义上,五线谱是音乐的语言,音乐演奏者是这种语言的解释器
软件的例子:
内部DSL在软件方面的例子有很多。PyT是一个用于生成HTML的Python-DSL。PyT关注性能,并声称能与Jinja2的速度相媲美。当然,我们不能假定在PyT。中必须使用解释器模式。然而,PyT是一种内部DSL,非常适合使用解释器模式。
.
Chromium是一个*开源的浏览器软件,催生出了Google-Chrome浏览器。Chromium的Mesa库Python绑定的一部分使用解释器模式将C样板参数翻译成 Python对象并执行相关的命令。
应用案例:
在我们希望为领域专家和高级用户提供一种简单语言来解决他们的问题时,可以使用解释器模式。
.
我们的目标是为专家提供恰当的编程抽象,使其生产力更高;这些专家通常不是程序员。理想情况下,他们使用我们的DSL并不需要了解高级Python知识,当然了解一点Python基础知识会更好,因为我们最终生成的是Python代码,但不应该要求了解Python高级概念。此外,DSL的性 能通常不是一个重要的关注点。重点是提供一种语言,隐藏宿主语言的独特性,并提供人类更易读的语法。诚然,Python已经是一门可读性非常高的语言,与其他编程语言相比,其古怪的语法更少。
代码实现:
from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums
class Gate:
def __init__(self):
self.is_open = False
def __str__(self):
return 'open' if self.is_open else 'closed'
def open(self):
print('opening the gate')
self.is_open = True
def close(self):
print('closing the gate')
self.is_open = False
class Garage:
def __init__(self):
self.is_open = False
def __str__(self):
return 'open' if self.is_open else 'closed'
def open(self):
print('opening the garage')
self.is_open = True
def close(self):
print('closing the garage')
self.is_open = False
class Aircondition:
def __init__(self):
self.is_on = False
def __str__(self):
return 'on' if self.is_on else 'off'
def turn_on(self):
print('turning on the aircondition')
self.is_on = True
def turn_off(self):
print('turning off the aircondition')
self.is_on = False
class Heating:
def __init__(self):
self.is_on = False
def __str__(self):
return 'on' if self.is_on else 'off'
def turn_on(self):
print('turning on the heating')
self.is_on = True
def turn_off(self):
print('turning off the heating')
self.is_on = False
class Boiler:
"""
Boiler类。一个锅炉的默认温度为83摄氏度。类有两个方法来分别提高和降低当前的温度。
"""
def __init__(self):
self.temperature = 83 # in celsius
def __str__(self):
return 'boiler temperature: {}'.format(self.temperature)
def increase_temperature(self, amount):
print("increasing the boiler's temperature by {} degrees".format(amount))
self.temperature += amount
def decrease_temperature(self, amount):
print("decreasing the boiler's temperature by {} degrees".format(amount))
self.temperature -= amount
class Fridge:
def __init__(self):
self.temperature = 2 # 单位为摄氏度
def __str__(self):
return 'fridge temperature: {}'.format(self.temperature)
def increase_temperature(self, amount):
print("increasing the fridge's temperature by {} degrees".format(amount))
self.temperature += amount
def decrease_temperature(self, amount):
print("decreasing the fridge's temperature by {} degrees".format(amount))
self.temperature -= amount
def main():
# 我们使用巴科斯-诺尔形式(Backus-Naur Form,BNF)表示法来定义语法。
# 这个语法告诉我们的是一个事件具有 command -> receiver -> arguments 的形式,
# 并且命令、接收者及参数也具有相同的形式,
word = Word(alphanums)
command = Group(OneOrMore(word))
token = Suppress("->")
device = Group(OneOrMore(word))
argument = Group(OneOrMore(word))
event = command + token + device + Optional(token + argument)
# 实例化类对象
gate = Gate()
garage = Garage()
airco = Aircondition()
heating = Heating()
boiler = Boiler()
fridge = Fridge()
# 事件语法
tests = ('open -> gate',
'close -> garage',
'turn on -> aircondition',
'turn off -> heating',
'increase -> boiler temperature -> 5 degrees',
'decrease -> fridge temperature -> 2 degrees')
# 开启操作字典
open_actions = {'gate': gate.open,
'garage': garage.open,
'aircondition': airco.turn_on,
'heating': heating.turn_on,
'boiler temperature': boiler.increase_temperature,
'fridge temperature': fridge.increase_temperature
}
# 关闭操作字典
close_actions = {'gate': gate.close,
'garage': garage.close,
'aircondition': airco.turn_off,
'heating': heating.turn_off,
'boiler temperature': boiler.decrease_temperature,
'fridge temperature': fridge.decrease_temperature
}
for t in tests:
# 判断解析结果,2个结果表示事件 没有参数
if len(event.parseString(t)) == 2:
cmd, dev = event.parseString(t)
cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
if 'open' in cmd_str or 'turn on' in cmd_str:
open_actions[dev_str]()
elif 'close' in cmd_str or 'turn off' in cmd_str:
close_actions[dev_str]()
# 判断解析结果,3个结果表示事件 有参数
elif len(event.parseString(t)) == 3:
cmd, dev, arg = event.parseString(t)
cmd_str, dev_str, arg_str = ' '.join(cmd), ' '.join(dev), ' '.join(arg)
num_arg = 0
try:
num_arg = int(arg_str.split()[0]) # 抽取数值部分
except ValueError as err:
print("expected number but got: '{}'".format(arg_str[0]))
if 'increase' in cmd_str and num_arg > 0:
open_actions[dev_str](num_arg)
elif 'decrease' in cmd_str and num_arg > 0:
close_actions[dev_str](num_arg)
if __name__ == '__main__':
main()
解析器模式总结:
解释器模式用于为高级用户和领域专家提供一个类编程的框架,但没有暴露出编程语言那样的复杂性。这是通过实现一个DSL来达到目的的
.
DSL是一种针对特定领域、表达能力有限的计算机语言。DSL有两类,分别是内部DSL和外部DSL。内部DSL构建在一种宿主编程语言之上,依 赖宿主编程语言,外部DSL则是从头实现,不依赖某种已有的编程语言。解释器模式仅与内部DSL相关。
.
乐谱是一个非软件DSL的例子。音乐演奏者像一个解释器那样,使用乐谱演奏出音乐。从软件的视角来看,许多Python模板引擎都使用了内部DSL。PyT是一个高性能的生成(X)HTML的Python-DSL。我们也看到Chromium的Mesa库是如何使用解释器模式将图形相关的C代码翻译成 Python可执行对象的。
④.观察者模式:
简介:
有时,我们希望在一个对象的状态改变时更新另外一组对象。在MVC模式中有这样一个非常常见的例子,假设在两个视图(例如,一个饼图和一个电子表格)中使用同一个模型的数据,无论何时更改了模型,都需要更新两个视图。这就是观察者设计模式要处理的问题。
.
观察者模式描述单个对象(发布者,又称为主持者或可观察者)与一个或多个对象(订阅者,又称为观察者)之间的发布—订阅关系。在MVC例子中,发布者是模型,订阅者是视图。
.
观察者模式背后的思想等同于MVC和关注点分离原则背后的思想,即降低发布者与订阅者之间的耦合度,从而易于在运行时添加或删除订阅者。此外,发布者不关心它的订阅者是谁。它只是将通知发送给所有订阅者。
现实生活的例子:
现实中,拍卖会类似于观察者模式。每个拍卖出价人都有一些拍牌,在他们想出价时就可以举起来。不论出价人在何时举起一块拍牌,拍卖师都会像主持者那样更新报价,并将新的价格广 播给所有出价人(订阅者)。
软件的例子:
大体上,所有利用MVC模式的系统都是基于事件的。
.
django-observer源代码包是一个第三方Django包,可用于注册回调函数,之后在某些Django模型字段发生变化时执行。它支持许多不同类型的模型字段 (CharField、IntegerField等)。
.
RabbitMQ可用于为应用添加异步消息支持,支持多种消息协议(比如,HTTP和AMQP),可在Python应用中用于实现发布—订阅模式,也就是观察者设计模式。
应用案例:
当我们希望在一个对象(主持者/发布者/可观察者)发生变化时通知或更新另一个或多个对象的时候,通常会使用观察者模式。观察者的数量以及谁是观察者可能会有所不同,也可以(在运行时)动态地改变。
.
同样的概念也存在于社交网络。如果你使用社交网络服务关联了另一个人,在关联的人更新某些内容时,你能收到相关通知,不论这个关联的人是你关注的一个Twitter用户,Facebook上的 一个真实朋友,还是LinkdIn上的一位同事。
.
事件驱动系统是另一个可以使用(通常也会使用)观察者模式的例子。在这种系统中,监听者被用于监听特定事件。监听者正在监听的事件被创建出来时,就会触发它们。这个事件可以是键入(键盘的)某个特定键、移动鼠标或者其他。事件扮演发布者的角色,监听者则扮演观察者的角色。在这里,关键点是单个事件(发布者)可以关联多个监听者(观察者)
代码实现:
class Publisher:
"""
Publisher类。观察者们保存在列表observers中。
add()方法注册一个新的观察者,或者在该观察者已存在时引发一个错误。
remove()方法注销一个已有观察者,或者在该观察者尚未存在时引发一个错误。
notify()方法则在变化发生时通知所有观察者
"""
def __init__(self):
self.observers = []
def add(self, observer):
if observer not in self.observers:
self.observers.append(observer)
else:
print('Failed to add: {}'.format(observer))
def remove(self, observer):
try:
self.observers.remove(observer)
except ValueError:
print('Failed to remove: {}'.format(observer))
def notify(self):
[o.notify(self) for o in self.observers]
class DefaultFormatter(Publisher):
"""
__init__()做的第一件事情就是调用基类的__init__() 方法,因为这在Python中没法自动完成。
DefaultFormatter实例有自己的名字,这样便于我们跟踪其状态。
"""
def __init__(self, name):
Publisher.__init__(self)
self.name = name
self._data = 0
def __str__(self):
"""
__str__()方法返回关于发布者名称和_data值的信息。
type(self).__name是一种获取类名的方便技巧,避免硬编码类名。这降低了代码的可读性,却提高了可维护性
"""
return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)
@property
def data(self):
"""
对于_data变量,我们使用了property装饰器来将data方法变成属性来得到私有属性_data的值。
"""
return self._data
@data.setter
def data(self, new_value):
"""
data()更有意思。它使用了@setter修饰器,该修饰器会在每次使用赋值操作符(=)为_data变量赋新值时被调用。
该方法也会尝试把新值强制类型转换为一个整数,并在类型转换失败时处理异常
"""
try:
self._data = int(new_value)
except ValueError as e:
print('Error: {}'.format(e))
else:
self.notify()
class HexFormatter:
"""观察者类1(十六进制)"""
def notify(self, publisher):
print("{}: '{}' has now hex data = {}".format(type(self).__name__, publisher.name, hex(publisher.data)))
class BinaryFormatter:
"""观察者类2(二进制)"""
def notify(self, publisher):
print("{}: '{}' has now bin data = {}".format(type(self).__name__, publisher.name, bin(publisher.data)))
def main():
"""
main()函数一开始创建一个名为test1的Default- Formatter实例,并在之后关联了两个可用的观察者。
也使用了异常处理来确保在用户输入问题数据时应用不会崩溃。此外,
诸如两次添加相同的观察者或删除尚不存在的观察者之类的事情也不应该导致崩溃。
"""
df = DefaultFormatter('test1')
print(df)
# 添加第一个观察者
print()
hf = HexFormatter()
df.add(hf)
df.data = 3
print(df)
#参加第二个观察者
print()
bf = BinaryFormatter()
df.add(bf)
df.data = 21
print(df)
# 删除一个观察者
print()
df.remove(hf)
df.data = 40
print(df)
# 删除另一个观察者后再添加一个观察者
print()
df.remove(hf)
df.add(bf)
# 异常捕获
df.data = 'hello'
print(df)
print()
df.data = 15.8
print(df)
if __name__ == '__main__':
main()
观察者模式总结:
若希望在一个对象的状态变化时能够通知或提醒所有相关者(一个对象或一组对象),则可以使用观察者模式。观察者模式的一个重要特性是,在运行时,订阅者/观察者的数量以及观察者是谁可能会变化,也可以改变。
⑤.状态模式:
简介:
面向对象编程着力于在对象交互时改变它们的状态。在很多问题中,有限状态机(通常名为状态机)是一个非常方便的状态转换建模(并在必要时以数学方式形式化)工具。
.
状态机是一个抽象机器,有两个关键部分,状态和转换。状态是指系统的当前(激活)状况。。转换是指从一个状态切换到另一个状态,因某个事件或条件的触发而开始,通常,在一次转换发生之前或之后会执行一个或一组动作。
.
进程一开始由用户创建好,就进入“已创建或新建”状态。这个状态只能切换到“等待”状态,这个状态转换发生在调度器将进程加载进内存并添加到“等待/预备执行”的进程队列之时。一个“等待”进程有两个可能的状态转换:可被选择而执行(切换到“运行”状态),或被更高优先级的进程所替代(切换到“换 出并等待”状态)。
现实生活的例子:
自动售货机有不同的状态,并根据我们放入的钱币数量作出不同反应。根据我们的选 择和放入的钱币,机器会执行以下操作。
1. 拒绝我们的选择,因为请求的货物已售罄。
2. 拒绝我们的选择,因为放入的钱币不足。
3. 递送货物,且不找零,因为放入的钱币恰好足够。
4. 递送货物,并找零。
软件的例子:
使用状态模式本质上相当于实现一个状态机来解决特定领域的一个软件问题。django-fsm程序包是一个第三方程序包,用于Django框架中简化状态机的实现和使用。
.
另一个值得一提的项目是状态机编译器(State Machine Compiler,SMC)。使用SMC,你可以使用一种简单的领域特定语言在文本文件中描述你的状态机,SMC会自动生成状态机的代码。该项目声称这种DSL非常简单,写起来就像一对一地翻译一个状态图。
应用案例:
1. 状态模式适用于许多问题。所有可以使用状态机解决的问题都是不错的状态模式应用案例。 我们已经见过的一个例子是操作系统/嵌入式系统的进程模型。
.
2. 编程语言的编译器实现是另一个好例子。词法和句法分析可使用状态来构建抽象语法树。
.
3. 事件驱动系统也是另一个例子。在一个事件驱动系统中,从一个状态转换到另一个状态会触发一个事件或消息。许多计算机游戏都使用 这一技术。例如,怪兽会在主人公接近时从防御状态 转换到攻击状态
代码实现:
from state_machine import State, Event, acts_as_state_machine, after, before, InvalidStateTransition
# 每个创建好的进程都有自己的状态机。使用state_machine模块
# 创建状态机的第一个步骤是使用@acts_as_state_machine修饰器。
@acts_as_state_machine
class Process:
"""
定义状态机的状态。这是我们在状态图中看到的节点的映射。
唯一的区别是应指定 状态机的初始状态。这可通过设置inital=True来指定。
"""
created = State(initial=True)
waiting = State()
running = State()
terminated = State()
blocked = State()
swapped_out_waiting = State()
swapped_out_blocked = State()
# 接着定义状态转换。在state_machine模块中,一个状态转换就是一个Event。
# 我们使用参数from_states和to_state来定义一个可能的转换。from_states可以是单个状态或一组状态(元组)。
wait = Event(from_states=(created, running, blocked, swapped_out_waiting), to_state=waiting)
run = Event(from_states=waiting, to_state=running)
terminate = Event(from_states=running, to_state=terminated)
block = Event(from_states=(running, swapped_out_blocked), to_state=blocked)
swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting)
swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)
# 每个进程都有一个名称。正式的应用场景中,一个进程需要多得多的信息才能发挥其作用(例 如,ID、优先级和状态等),
# 但为了专注于模式本身,我们进行一些简化,只初始化对应的name
def __init__(self, name):
self.name = name
# 在发生状态转换时,如果什么影响都没有,那转换就没什么用了。state_machine模块提 供@before和@after修饰器,
# 用于在状态转换之前或之后执行动作。为了达到示例的目的,这 里的动作限于输出进程状态转换的信息。
@after('wait')
def wait_info(self):
print('{} entered waiting mode'.format(self.name))
@after('run')
def run_info(self):
print('{} is running'.format(self.name))
@after('terminate')
def terminate_info(self):
print('{} terminated'.format(self.name))
@after('block')
def block_info(self):
print('{} is blocked'.format(self.name))
@after('swap_wait')
def swap_wait_info(self):
print('{} is swapped out and waiting'.format(self.name))
@after('swap_block')
def swap_block_info(self):
print('{} is swapped out and blocked'.format(self.name))
def transition(process, event, event_name):
"""
transition()函数接受三个参数:process、event和event_name。在尝试执行event时,如果发生错误,则会输出事件的名称。
:param process:是一个Process类实例,
:param event: event是一个Event类(wait、run和terminate等)实例,
:param event_name: 而event_name 是事件的名称。
"""
try:
event()
except InvalidStateTransition as err:
print('Error: transition of {} from {} to {} failed'.format(process.name, process.current_state, event_name))
def state_info(process):
"""
state_info()函数展示进程当前(激活)状态的一些基本信息。
"""
print('state of {}: {}'.format(process.name, process.current_state))
def main():
# 在main()函数的开始,我们定义了一些字符串常量,作为event_name参数值传递。
RUNNING = 'running'
WAITING = 'waiting'
BLOCKED = 'blocked'
TERMINATED = 'terminated'
# 我们创建两个Process实例,并输出它们的初始状态信息。
p1, p2 = Process('process1'), Process('process2')
[state_info(p) for p in (p1, p2)]
# 其余部分将尝试不同的状态转换。回忆一下本章之前提到的状态图。允许的状态转换应与状态图一致。
# 例如,从状态“运行”转换到状态“阻塞”是可能的,但从状态“阻塞”转换 到状态“运行”则是不可能的。
print()
transition(p1, p1.wait, WAITING)
transition(p2, p2.terminate, TERMINATED)
[state_info(p) for p in (p1, p2)]
print()
transition(p1, p1.run, RUNNING)
transition(p2, p2.wait, WAITING)
[state_info(p) for p in (p1, p2)]
print()
transition(p2, p2.run, RUNNING)
[state_info(p) for p in (p1, p2)]
print()
[transition(p, p.block, BLOCKED) for p in (p1, p2)]
[state_info(p) for p in (p1, p2)]
print()
[transition(p, p.terminate, TERMINATED) for p in (p1, p2)]
[state_info(p) for p in (p1, p2)]
if __name__ == '__main__':
main()
状态模式总结:
状态模式是一个或多个有限状态机(简称状态机)的实 现,用于解决一个特定的软件工程问题。
.
状态机是一个抽象机器,具有两个主要部分:状态和转换。状态是指一个系统的当前状况。一个状态机在任意时间点只会有一个激活状态。转换是指从当前状态到一个新状态的切换。在一个转换发生之前或之后通常会执行一个或多个动作。状态机可以使用状态图进行视觉上的展现。
.
状态机用于解决许多计算机问题和非计算机问题,其中包括交通灯、停车计时器、硬件设计和编程语言解析等。我们也看到零食自动贩卖机是如何与状态机的工作方式相关联的。
.
许多现代软件提供库/模块来简化状态机的实现与使用。Django提供第三方包django-fsm,Python也有许多大家贡献的模块。实际上,在14.4节就使用了其中的一个模块(state_machine)。 状态机编译器是另一个有前景的项目,提供许多编程语言的绑定(包括Python)。
.
状态模式是一个或多个有限状态机(简称状态机)的实 现,用于解决一个特定的软件工程问题。
.
状态机是一个抽象机器,具有两个主要部分:状态和转换。状态是指一个系统的当前状况。一个状态机在任意时间点只会有一个激活状态。转换是指从当前状态到一个新状态的切换。在一 个转换发生之前或之后通常会执行一个或多个动作。状态机可以使用状态图进行视觉上的展现。
.
状态机用于解决许多计算机问题和非计算机问题,其中包括交通灯、停车计时器、硬件设计和编程语言解析等。我们也看到零食自动贩卖机是如何与状态机的工作方式相关联的。
⑥.策略模式:
简介:
大多数问题都可以使用多种方法来解决。以排序问题为例,对于以一定次序把元素放入一个列表,排序算法有很多。通常来说,没有公认适合所有场景的算法。 一些不同的评判标准能帮助我们为不同的场景选择不同的排序算法,其中应该考虑的有以下几个:
.
1. 需要排序的元素数量:这被称为输入大小。当输入较少时,几乎所有排序算法的表现都很好,但对于大量输入,只有部分算法具有不错 的性能。
2. 算法的最佳/平均/最差时间复杂度:时间复杂度是算法运行完成所花费的(大致)时间长短,不考虑系数和低阶项。这是选择算法的常 见标准,但这个标准并不总是那么充分。
3. 算法的空间复杂度:空间复杂度是充分地运行一个算法所需要的(大致)物理内存量。在我们处理大数据或在嵌入式系统(通常内存有 限)中工作时,这个因素非常重要。
4. 算法的稳定性:在执行一个排序算法之后,如果能保持相等值元素原来的先后相对次序, 则认为它是稳定的。
5. 算法的代码实现复杂度:如果两个算法具有相同的时间或空间复杂度,并且都是稳定的,那么知道哪个算法更易于编码实现和维护也是 很重要的。
.
策略模式的目的:
可能还有更多的评判标准值得考虑,但重要的是,我们真的只能使用单个排序算法来应对所有情况吗?答案当然不是。一个更好的方案是把所有排序算法纳为己用,然后使用上面提到的标准针对当前情况选择好的算法。这就是策略模式的目的。
.
策略模式(Strategy pattern)
鼓励使用多种算法来解决一个问题,其杀手级特性是能够在运行时透明地切换算法(客户端代码对变化无感知)。因此,如果你有两种算法,并且知道其中一种对少量输入效果更好,另一种对大量输入效果更好,则可以使用策略模式在运行时基于输入数据决定使用哪种算法。
现实生活的例子:
去机场赶飞机是现实中使用策略模式的一个恰当例子。
.
1. 如果想省钱,并且早点出发,那么可以坐公交车/地铁。
2. 如果不介意支付停车费,并且有自己的汽车,那么可以开车去。
3. 如果没有自己的车,又比较急,则可以打车。
这是费用、时间、便利性等因素之间的一个折中权衡。
软件的例子:
Python 的 sorted() 和 list.sort()函数是策略模式的例子。两个函数都接受一个命名参数key,这个参数本质上是实现了一个排序策略的函数的名称.
应用案例:
策略模式是一种非常通用的设计模式,可应用的场景很多。一般来说,不论何时希望动态、透明地应用不同算法,策略模式都是可行之路。这里所说不同算法的意思是,目的相同但实现方案不同的一类算法。这意味着算法结果应该是完全一致的,但每种实现都有不同的性能和代码复杂性(举例来说,对比一下顺序查找和二分查找)。
.
策略模式并不限于排序问题,也可用于创建各种不同的资源过滤器(身份验证、日志记录、数据压缩和加密等)
.
策略模式的另一个应用是创建不同的样式表现,为了实现可移植性(例如,不同平台之间断行的不同)或动态地改变数据的表现。
.
另一个值得一提的应用是模拟;例如模拟机器人,一些机器人比另一些更有攻击性,一些机器人速度更快,等等。机器人行为中的所有不同之处都可以使用不同的策略来建模。
代码实现:
以下代码展示了如何用以下方式使用两种不同的策略对编程语 言进行排序。
import pprint
from collections import namedtuple
from operator import attrgetter
# pprint模块用于美化输出一个数据结构, attrgetter用于通过属性名访问class或namedtuple的属性。
# 也可以使用一个lambda函数来 替代使用attrgetter,但我觉得attrgetter的可读性更高。
if __name__ == '__main__':
ProgrammingLang = namedtuple('ProgrammingLang', ("name", "ranking"))
stats = (('Ruby', 14), ('Javascript', 8), ('Python', 7), ('Scala', 31), ('Swift', 18), ('Lisp', 23))
lang_stats = [ProgrammingLang(n, r) for n, r in stats]
pp = pprint.PrettyPrinter(indent=5) # indent 每个嵌套层要缩进的空格数量
pp.pprint(sorted(lang_stats, key=attrgetter('name')))
print()
pp.pprint(sorted(lang_stats, key=attrgetter('ranking')))
策略模式的实现代码
import time
SLOW = 3 # 单位为秒
LIMIT = 5 # 字符数
WARNING = 'too bad, you picked the slow algorithm :('
def pairs(seq):
"""
它会返回所有相邻字符对的一个序列seq
"""
n = len(seq)
for i in range(n):
yield seq[i], seq[(i + 1) % n]
def allUniqueSort(s):
"""
它接受一个字符串参数s,如果该字符串中所有字符 都是唯一的,则返回True;否则,返回False。
为演示策略模式,我们进行一些简化,假设这个算法的伸缩性不好,对于不超过5个字符的字符串才能工作良好。
对于更长的字符串,通过插入 一条sleep语句来模拟速度减缓。
"""
if len(s) > LIMIT:
print(WARNING)
time.sleep(SLOW)
srtStr = sorted(s)
for (c1, c2) in pairs(srtStr):
if c1 == c2:
return False
return True
def allUniqueSet(s):
"""
我们使用一个集合来实现算法。 如果正在检测的字符已经被插入到集合中,则意味着字符串中并非所有字符都是唯一的
"""
if len(s) < LIMIT:
print(WARNING)
time.sleep(SLOW)
return True if len(set(s)) == len(s) else False
def allUnique(s, strategy):
return strategy(s)
def main():
"""
用main()函数可以执行以下操作。
1. 输入待检测字符唯一性的单词
2. 选择要使用的策略
该函数还进行了一些基本的错误处理,并让用户能够正常退出程序
"""
while True:
word = None
while not word:
word = input('Insert word (type quit to exit)> ')
if word == 'quit':
print('bye')
return
strategy_picked = None
strategies = {'1': allUniqueSet, '2': allUniqueSort}
while strategy_picked not in strategies.keys():
strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')
try:
strategy = strategies[strategy_picked]
print('allUnique({}): {}'.format(word,
allUnique(word, strategy)))
except KeyError as err:
print('Incorrect option: {}'.format(strategy_picked))
print()
if __name__ == '__main__':
main()
策略模式总结:
策略模式通常用在我们希望对同一个问题透明地使用多种方案时。如果并不存在针对所有输入数据和所有情况的完美算法,那么我们可以使用策略模式, 动态地决定在每种情况下应使用哪种算法。现实中,在我们想赶去机场乘飞机时会使用策略模式。
.
Python使用策略模式让客户端代码决定如何对一个数据结构中的元素进行排序。我们看到了一个例子,基于TIOBE指数排行榜对编程语言进行排序。
.
策略设计模式的使用并不限于排序领域。加密、压缩、日志记录及其他资源处理的领域都可以使用策略模式来提供不同的数据处理方式。可移植性是策略模式的另一个用武之地。模拟也是 另一个策略模式适用的领域。
.
通过实现两种不同算法来检测一个单词中所有字符的唯一性,我们学习了Python如何因其具 有一等函数而简化了策略模式的实现。
⑦.模板模式:
简介:
编写优秀代码的一个要素是避免冗余。在面向对象编程中,方法和函数是我们用来避免编写冗余代码的重要工具。
.
模板设计模式(Template design pattern)。这个模式关注的是消除代码冗余,其思想是我们应该无需改变算法结构就能重新定义一个算法的某些部分。为了避免重复而进行必要的重构。
现实生活的例子:
工人的日程,特别是对于同一个公司的工人而言,非常接近于模板设计模式。所有工人都遵从或多或少相同的例行流程,但例行流程的某些特定部分区别又很大。
软件的例子:
Python在cmd模块中使用了模板模式,该模块用于构建面向行的命令解释器。具体而言, cmd.Cmd.cmdloop()实现了一个算法,持续地读取输入命令并将命令分发到动作方法。每次循环之前、之后做的事情以及命令解析部分始终是相同的。这也称为一个算法的不变部分。变化的 是实际的动作方法(易变的部分)。
.
Python的asyncore模块也使用了模板模式,该模块用于实现异步套接字服务客户端、服务器。其中诸如asyncore.dispatcher.handle_connect_event和asyncore.dispatcher. handle_write_event()之类的方法仅包含通用代码。要执行特定于套接字的代码,这两个方 法会执行handle_connect()方法。注意,执行的是一个特定于套接字的handle_connect(),不是asyncore.dispatcher.handle_connect()。后者仅包含一条警告。可以使用inspect模块来查看。
应用案例:
模板设计模式旨在消除代码重复。如果我们发现结构相近的(多个)算法中有重复代码,则可以把算法的不变(通用)部分留在一个模板方法/函数中,把易变(不同)的部分移到动作/钩 子方法/函数中。
.
页码标注是一个不错的模板模式应用案例。一个页码标注算法可以分为一个抽象(不变的)部分和一个具体(易变的)部分。不变的部分关注的是大行号/页号这部分内容。易变的部分 则包含用于显示某个已分页特定页面的页眉和页脚的功能。
.
所有应用框架都利用了某种形式的模板模式。在使用框架来创建图形化应用时,通常是继承自一个类,并实现自定义行为。然而,在执行自定义行为之前,通常会调用一个模板方法,该方法实现了应用中一定相同的部分,比如绘制屏幕、处理事件循环、调整窗口大小并居中等等
代码实现:
from cowpy import cow
"""
我们将实现一个横幅生成器。想法很简单,将一段文本发送给一个函数,该函数要 生成一个包含该文本的横幅。
横幅有多种风格,比如点或虚线围绕文本。横幅生成器有一个默认风格,但应该能够使用我们自己提供的风格。
"""
def dots_style(msg):
"""
dots_style()简单地将msg首字母大写,并在其之前和之后输出10个点
"""
msg = msg.capitalize()
msg = '.' * 10 + msg + '.' * 10
return msg
def admire_style(msg):
msg = msg.upper()
return '!'.join(msg)
def generate_banner(msg, style=dots_style):
"""
数generate_banner()是我们的模板函数。它接受一个输入参数(msg,希望横幅包含的文本)和一个可选参数(style,希望使用的风格)。
默认风格是dots_style,我们马上就能 看到。generate_banner()以一个简单的头部和尾部来包装带样式的文本。
实际上,这个头部和尾部可以复杂得多,但在这里调用可以生成头部和尾部的函数来替代仅仅输出简单字符串也无不可。
"""
print('-- start of banner --')
print(style(msg))
print('-- end of banner --\n\n')
def cow_style(msg):
"""
cow_style()风格使用cowpy模块生成随机ASCII码艺 术字符,夸张地表现文本
"""
msg1 = cow.milk_random_cow(msg)
msg2 = cow.milk_random_cow(msg1)
return msg2
def main():
"""
main()函数向横幅发送文本“关关之舟,在河之洲!”,并使用所有可用风格将横幅输出到标准输出。
"""
msg = '关关之舟,在河之洲!'
[generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]
if __name__ == '__main__':
main()
模板模式总结:
我们学习了模板设计模式。在实现结构相近的算法时,可以使用模板模式来消除冗 余代码。具体实现方式是使用动作/钩子方法/函数来完成代码重复的消除,它们是Python中的一 等公民。我们学习了一个实际的例子,即使用模板模式来重构BFS和DFS算法的代码。