设计模式 with Python 15:其它模式(上)
《Head First 设计模式》在附录中介绍了剩余的一些设计模式,在这里予以介绍和总结。
桥接模式
为了说明桥接模式,假设我们需要给不同品牌不同型号的电视机设计一款通用的遥控器,最简单最容易想到的设计应该是这样的:
我们通过一致性的接口RemoteControl
来抽象了一个通用的遥控器“外观”,只要让具体的电视实现这个接口就行了。
但这样的设计在面临某些问题时缺乏可扩展性,比如如果我们需要开发出不同用户界面的遥控器,有的是目前的四案件,但有的要有数字按键,可以快速将电视切换到设置好的频道。也就是说我们需要多个“遥控器外观”。
这时候我们需要怎么做?
我们可以像上面的UML中展示的那样,将“遥控器”和“电视”这两个概念完全分离,它们不再具有上下级的继承关系,而是平行的两组概念,左边的遥控器RemoteControl
及子类负责实现各种类型的遥控器,而右侧的TV
及子类负责实现不同类型和品牌的电视,而遥控器和电视之间的连系体现在RemoteControl
所持有的一个TV
的引用tv
,通过这个引用来实现具体的遥控器功能。
这个设计的精髓在于,我们分离这两个原本混合在一起的概念以后,可以对这两种概念分别进行修改和扩展,并且不会影响到彼此(或者影响很小),就像上面展示的那样,我们可以很容易地在RemoteControl
下面扩展出一个数字键盘遥控器NumberControl
,并且不需要修改已有的电视相关的类。
上面的这种设计就叫做桥接模式。
现在引入桥接模式的定义:
使用桥接模式(Bridge Pattern)不只改变你的实现,也改变你的抽象。
它体现了下面几个设计原则:
-
单一职能原则
在原本的设计中,电视类
Sony
和Xiaomi
直接实现遥控器RemoteControl
接口,此时电视的类就承担了两个不同的职能:控制电视和提供遥控器接口。使用桥接模式后我们将这两个不同的功能进行了分离。 -
好莱坞原则
高层组件
RemoteControl
负责调用底层组件TV
,而没有反过来的调用,保持了系统不同层次组件间的简洁。 -
多用组合少用继承
-
对接口编程而非实现
RemoteControl
中使用的电视引用类型为抽象基类TV
,而非具体的电视。
桥接模式适合用于跨越多个平台和系统的窗口和图形应用。
如之前的示例所说明的那样,桥接模式的优点是将事物的抽象(遥控器)与实现(电视)进行解耦,降低其耦合度,因此使得抽象和实现可以分别扩展的同时可以不互相影响。
桥接模式的缺点在于会增加系统的复杂度。
虽然我觉得UML表示的已经很清楚了,示例代码帮助不大,但我还是完成了一个示例代码bridge,感兴趣的可以自行查看。
建造模式
原书翻译的是生成器模式,但我认为并不准确,所以我这里使用自创的称呼:建造模式。
同样的,为了引入建造模式,我们要假设我们需要创建一个旅游行程规划工具,这个工具要可以制定多天行程,并且可以在每天行程中添加机票、酒店、景点门票等。
当然我们可以分别创建类表示行程、机票、酒店、门票,再一一用合适的数据结构组合起来,但如果每次都要这样去创建行程无疑是相当繁琐和不直观的。
所以我们可以把行程创建过程用一种直观的方式进行封装:
这里我们将所有面向客户端程序的行程创建过程都封装到了TripBUilderInterface
中,通过调用其提供的方法,我们可以创建一个包含多天旅行计划的行程。当然这里只显示了相关关键类,在具体实现中可能还需要创建酒店、门票等相关类。
完整的示例代码见generator。
现在引入生成模式的定义:
建造模式(Builder Pattern)封装一个产品的构造过程,并允许按步骤构造。
建造模式有点像外观模式,但是生成器模式更注重的是将需要经过一系列复杂调用来组织创建某个对象的行为进行封装,而非是对一个复杂系统的调用进行简单封装。前者更注重创建行为。
和工厂方法相比,两者目的相同,都是封装对象创建过程,但显然和只要调用一个方法就可以创建所需对象的工厂方法相比,建造模式更复杂,需要调用一系列方法,但同时后者也更灵活,可以根据需要创建出不同的对象。
此外,建造模式往往会用于创建复杂的组合就够,就像上面展示的那样,Trip
包含Day
,Day
包含其它的类型数据。
建造模式的缺点在于为了创建恰当的组合结构,需要深入的分析需求和业务,这需要花费额外的时间和精力。
责任链模式
这是一个相对简单的模式。
假设我们需要创建一个文件分类系统,对给定的文件进行分类归档,当然我们可以像常规那样,写一堆if/else
进行处理,但除此以外,我们可以用一个更有弹性的设计来解决:
在上面这个设计中,VedioClassify
、PictureClassify
、OtherClassify
构成了一个责任链,它们在ClassifyManager
中组成一个链状结构,对给定的一个文件File
对象,可以依次调用责任链上的对象的classify
方法进行文件分类工作,如果成功分类,返回True
并将分类结构存放在FileClassCollection
对象中,如果失败,则调用责任链上的下一个对象进行处理,依次进行,直到最后一个处理完。
需要注意的是有可能会有责任链上的所有对象都处理失败的情况,此时待处理的任务或者对象就会不经处理的留下来,这可能是个bug,也可能是你有意为之,具体要看你的设计和意图,但你也以像上面的OtherClassify
那样,给责任链的最后添加一个“万能”的处理对象,不让这种情况发生。
完整的示例代码见chain。
责任链的定义为:当你想让一个以上的对象有机会处理某个请求的时候,就使用责任链模式(Chain of Responsibility Pattern)。
可以看到责任链模式可以让请求的处理结构变得非常灵活,可以很容易地在责任链上添加或者删除某个处理对象,或者改变处理对象的排列顺序。
责任链经常用于窗口系统中,处理鼠标和键盘的监听事件。
责任链的缺点在于比普通的if/else
更为复杂,会给debug带来一定困难。
蝇量模式
当年我学习C++的时候,老师多次提醒对象创建本身是需要额外资源的,因为对象需要申请额外的空间用于保存一些对象所需的数据,比如所属的类等。而蝇量模式就是为节省对象创建而消耗大量的空间而产生的模式。
假设我们现在需要创建一个用于设计景观的应用,这个应用可以在面板中绘制添加的景观元素,比如若干颗树。
正常的设计可能是这样的:
class Tree:
def __init__(self, x: int, y: int, age: int) -> None:
self._x = x
self._y = y
self._age = age
def display(self):
print("draw in screen ({},{}) a tree, age is {}.".format(
self._x, self._y, self._age))
以这样的方式调用:
from random import randint
from fly_weight.src.tree_manager import TreeManager
from fly_weight.src.tree import Tree
for i in range(10):
x = randint(1,1920)
y = randint(1,1080)
age = randint(1,200)
tree = Tree(x,y,age)
tree.display()
如果Tree
的对象数目不多,或许没有什么问题,但如果成千上万,可能会对系统的负载造成压力,这种情况下我们就需要考虑以某种方式节约资源占用,比如说减少因为对象创建所产生的内存开销。
from collections import namedtuple
Tree = namedtuple("Tree", "x,y,age")
class TreeManager:
def __init__(self) -> None:
self._trees = list()
def addTree(self, x, y, age):
self._trees.append(Tree(x, y, age))
def _displayTree(self, x: int, y: int, age: int) -> None:
print("draw in screen ({},{}) a tree, age is {}.".format(
x, y, age))
def display(self):
tree: Tree
for tree in self._trees:
self._displayTree(tree.x, tree.y, tree.age)
这里我们使用TreeManager
来绘制所有的树,并且在内部使用比对象内存开销更小的数据结构存储每一颗树,这里我们使用的是具名数组,因为具名数组的内存开销接近于元组,在Python的数据结构中算是内存占用较少的一种。
新设计下的调用方式如下:
tManager = TreeManager()
for i in range(10):
x = randint(1, 1920)
y = randint(1, 1080)
age = randint(1, 200)
tManager.addTree(x, y, age)
tManager.display()
这就是蝇量模式。
完整的示例代码见fly_weight。
蝇量模式(Flyweight Pattern)就是使用一个实例来提供许多个“虚拟实例”的设计。
就像上面示例中那样,我们使用一个TreeMangaer
实例完成了原本需要许许多多Tree
实例所完成的任务,这样做的动力可能是因为性能优化或者别的什么原因。
但这样做不是没有条件的,这样做的前提条件是被替代的实例具有的行为都可以用统一的方法来替换,即它们不能有特异化的不同于其它实例的行为。这很好理解,如果有那样的实例我们就无法在TreeManager
中统一绘制。
本来我想在一篇文章中结束的,翻了翻还有5个模式,如果都放在这一篇里也显得略长,可能会影响阅读,放在下一篇吧,谢谢阅读。