设计模式 with Python3:装饰器模式
在撰写Python学习笔记系列文章的时候,函数装饰器和类装饰器提到过很多次,这也是Python编程中比较难理解和进阶的内容。当时我也顺便提到其本质是设计模式中的装饰器模式,那么今天我们就看一下经典设计模式中的装饰器模式到底是怎样的。
关闭开放原则
随着设计模式的学习深入,你就会发现设计模式的本质其实上是将复杂问题中的不可变部分和可变部分进行区分和封装,进而达到一种对不可变部分进行充分复用,对可变部分进行灵活扩展的程度。
当然,这在实际解决问题的时候你就会发现是一种相当理想化且极其艰难的事情,但好在我们已经有大佬们总结出的设计模式了,不是吗?
具体到装饰器模式,其实解决同样的是这类问题,其总结为设计原则就是:对修改关闭,对扩展开放。
这句话相当不好理解,我们来看具体事例。
《Head First 设计模式》一书本章节使用的是咖啡菜单作为例子,但我实在对咖啡没有研究,所以就用相对熟悉一点的武器装备作为例子了,但我同样连伪军迷也算不上,所以大家不要认真,就当图一乐就行。关键还是要说清楚装饰器模式。
假如我们要开发一个给我军的外贸坦克进行报价的应用,如果稍微对坦克这种装备熟悉点的朋友应该知道,现代主战坦克裸车并不是很贵,这东西就和买家用车差不多,可以根据自身情况进行配件加减,比如红外对抗装置,自动武器站,反反坦克导弹自动发射器等等,当然了,这些东西都是要钱的,而且价格不菲。比如非洲狗大户,自然直接顶配,东南亚越南之流当然是能省则省。
如果不考虑后期的维护和扩展性,最简单粗暴的方式无疑是这样编写代码:
from enum import Enum
class TankBody(Enum):
VT1 = 1
VT2 = 2
VT3 = 3
VT4 = 4
class Tank:
def __init__(self) -> None:
self.body:TankBody = TankBody.VT1
self.hasExtraArmour: bool = False #附加装甲
self.hasAutomaticWeaponStation: bool = False #自动武器站
self.hasAirConditioner: bool = False #空调
self.hasIRCM: bool = False #红外对抗
self.hasActiveDefence: bool = False #主动防御
def cost(self)->int:
"""坦克报价"""
price = 0
if self.body is TankBody.VT1:
price += 30000
elif self.body is TankBody.VT2:
price += 50000
elif self.body is TankBody.VT3:
price += 70000
elif self.body is TankBody.VT4:
price += 100000
else:
pass
if self.hasExtraArmour:
price += 10000
if self.hasAutomaticWeaponStation:
price += 20000
if self.hasAirConditioner:
price += 3000
if self.hasIRCM:
price += 30000
if self.hasActiveDefence:
price += 35000
return price
if __name__ == "__main__":
tank1 = Tank()
tank1.body = TankBody.VT4
tank1.hasActiveDefence = True
tank1.hasAirConditioner = True
tank1.hasAutomaticWeaponStation = True
tank1.hasExtraArmour = True
tank1.hasIRCM = True
print(tank1.cost())
当然了,稍微有点编程经验的人也不会写出这样的代码,这样的代码的坏处显而易见:扩展性和可维护性极差。
如果要增加一种坦克配置项,就需要修改Tank
类相关代码。
事实上更常见的做法是将坦克的可配置选项单独由一个容器保管,
cost
方法只要讲相关的配置价格相加即可。此外再添加一个方法给该容器动态添加配置项就可以了。这样做其实未尝不可,同样可以很灵活地组织和配置出一个外贸版本。当然了,我们这里不会采用这种方案,因为那样就没有后面的装配器模式啥事了,所以我们暂且加装不知道这种解决方案。
所以,我们要寻找一种不需要因为扩展需求而频繁修改Tank
类,而可以灵活扩展的方式,这就是前面提到的“对修改关闭,对扩展开放”。
使用继承扩展
最简单的扩展方式是继承,这是很容易想到的。
但是这种方式有很多缺陷,假设我们应客户需求要提供多个外贸坦克版本,那么整个系统的UML图可能会是这样:
当然,这个方案中实际上是给每一个客户定制的需求单独创建一个TankX
类,在这种情形下实际上并不需要hasXXX
这样的属性作为标记,之所以我这样做了,是为了表示每个TankX
之间的具体配置的不同。
这个方案的缺点显而易见,虽然说每一个TankX
类很灵活,客户想要什么样的需求我们都可以满足进行修改,但同样的,代码复用度可以说基本为零,而且更糟糕的是如果我们的产品很畅销,或者客户都是刺头,很容易导致我们的系统中的TankX
这种类的数量飞速膨胀,这将会给未来某一天的系统重构带来灾难性后果。
使用组合+委托扩展
除了上面的继承,在很多时候,我们可以使用组合+委托的形式对既有类型进行扩展。
比如如果我们要给一个坦克车体外挂上空调:
class Tank:
def cost(self)->int:
return 30000
class AirConditionerEquiped:
def __init__(self, tank: Tank) -> None:
self.tank: Tank = tank
def cost(self)->int:
return self.tank.cost()+3000
tank1 = AirConditionerEquiped(Tank())
print(tank1.cost())
使用这种简单的方式我们就可以创建一个装了空调的坦克,而且空调坦克的cost()
方法复用了包裹的tank
实例的cost()
方法,也就是用委托的方式进行了复用。
那么我们是不是可以依样画葫芦,用洋葱一样的结构包裹多层“附加类”来给客户定制一款外贸坦克?
这种想法正是装饰器模式的核心概念。
装饰器模式
装饰器模式的核心概念,就是对一个需要被装饰的基础类型,经过一系列装饰器类进行“装饰”,通过这种“动态”的方式,在不改变基础类型的代码的情况下,我们就可以给其添加上新的行为,这样做就可以实现前面所说的对修改关闭,对扩展开放。
可以将这种方式简单地想象为制作奶油蛋糕,基础类型就像一个蛋糕坯子,上边什么都没有,装饰器类就像是各种颜色的奶油、巧克力和水果,我们可以一层层添加各种佐料,做出一个我们想要的蛋糕。
我们回到卖坦克的问题,这里直接给出使用装饰器模式实现的方案的UML。
UML
这里的核心要点是,装饰器模式中所有的类都要有同一个基类,具体这个基类是接口还是抽象基类并不重要,重要的是因为它们具有相同的基类,所以可以看做是同一个类型,自然就可以随意地“涂奶油”,而不用担心你最后做出来的不是蛋糕,而是别的什么东西。
其次需要注意的是,原本无论是坦克车体还是用来装饰车体的附加模块,我们本可以直接实现Tank
接口就可以了,但是考虑到附加模块实质上都需要一个属性tank
持有“洋葱式”装饰层次中“内层”的Tank
实例,进而进行委托相应的方法。所以我们可以设置一个抽象层次ExtraEquipment
来实现这部分逻辑的复用。而TankBody
这个层次的抽象其实可有可无,因为作为“洋葱式”装饰层次最底层的坦克车体,并不需要持有Tank
实例,实现一些复杂些的逻辑。但是如果构建了这一层抽象也有一些额外的好处,比如如果我们需要从装饰层次中辨别最底层的类型,只要递归tank
属性,并检测tank
属性是否为TankBody
类型的实例即可。
具体实现
编程新手往往会痴迷于具体编码,事实上设计部分,也就是上面的UML才是程序设计中最消耗时间和精力的工作,和本系列上一篇文章一样,现在已经有了UML,这里我直接使用EA生成框架代码,在其上进行修改编码。
这里就不展示完整代码,仅显示比较核心的两个抽象层的代码:
#######################################################
#
# ExtraEquipment.py
# Python implementation of the Class ExtraEquipment
# Generated by Enterprise Architect
# Created on: 15-6��-2021 14:00:36
# Original author: 70748
#
#######################################################
from .Tank import Tank
import abc
class ExtraEquipment(Tank, abc.ABC):
def __init__(self, tank: Tank):
self.tank: Tank = tank
self.price: int = 0
self.des: str = ""
def cost(self):
return self.price + self.tank.cost()
def getDescription(self):
return self.tank.getDescription()+','+self.des
#######################################################
#
# TankBody.py
# Python implementation of the Class TankBody
# Generated by Enterprise Architect
# Created on: 15-6��-2021 14:00:36
# Original author: 70748
#
#######################################################
from .Tank import Tank
import abc
class TankBody(Tank, abc.ABC):
def __init__(self) -> None:
super().__init__()
self.des: str = ""
self.price: int = 0
def cost(self):
return self.price
def getDescription(self):
return self.des
进行测试:
from src.VT1 import VT1
from src.VT4 import VT4
from src.ActiveDefence import ActiveDefence
from src.AirConditioner import AirConditioner
from src.AutomaticWeaponStation import AutomaticWeaponStation
from src.ExtraArmour import ExtraArmour
from src.IRCM import IRCM
print("狗大户可以买这个:")
tank1 = IRCM(ExtraArmour(AutomaticWeaponStation(AirConditioner(ActiveDefence(VT4())))))
print(tank1.getDescription())
print(tank1.cost())
print("性价比可以买这个:")
tank2 = VT1()
tank2 = ExtraArmour(tank2)
tank2 = AutomaticWeaponStation(tank2)
print(tank2.getDescription())
print(tank2.cost())
# 狗大户可以买这个:
# VT4车体,主动防御系统,空调,自动武器站,附加装甲,红外对抗装置
# 198000
# 性价比可以买这个:
# VT1车体,附加装甲,自动武器站
# 60000
缺点
装饰器模式固然可以在不改变原有类型的基础上进行灵活扩展,但也有一些缺点:
- 用来装饰基础类型的“装饰类”会随着扩展变得数量很多。
- 使用的时候需要动态地临时组合,容易因为使用错误的“装饰类”而创建一个错误的最终类型。
前者是无法避免的,而后者可以使用工厂模式避免。
Python中的函数、类装饰器
如本文开头所说,Python中广泛使用的函数装饰器和类装饰器可以看做是装饰器模式的另类应用,而且层次很深,已经变成了Python语言特性的一部分。前者是对函数使用装饰器模式,通过创建函数装饰器,然后使用@
符号让Python解释器在运行时进行动态装饰,从而改变目标函数的特性。而后者的功能相仿,只不过装饰的目标不是函数,是类。
除了核心概念相似以外,我个人认为Python中的函数装饰器和类装饰器在实际使用中和经典的装饰器模式并不是很相似。
经典的装饰器模式的核心要点是动态地对对象进行“装饰”,以避免直接对基础类型进行继承或者直接修改。而Python中的函数装饰器、类装饰器是在不改变现有函数和类定义的情况下,直接通过相应的装饰器改变其行为,这更像是不用对类、函数进行继承或者其他方式的扩展的情况下,我们对其行进了功能扩展。
好了,关于装饰器模式的内容已经全部介绍完毕,谢谢阅读。
本系列文章的全部示例代码见Github项目design-pattern-with-python。