设计模式 with Python 7:适配器模式
在本系列的前几篇文章中我提到过,设计模式事实上是编程领域的前辈为了解决某一类问题总结出来的通用解决方案,而编程这项工作其实本身也是为了用计算机语言来描述和解决某些现实问题,所以设计模式面对的某一类问题及相应解决方案其实也可以在现实中找到对应的实体。
就像我们这篇文章要说的适配器模式,适配器在现实中其实并不少见。从中学物理会提到的电源适配器(实质上是变压器,将220V交流电转换为直流电)到“水货”电子产品必备的适配插头(一般叫旅行用万能转换插头)。
万能转换插头大概长这个样子:
前者的目的是为了将发电厂输送到用户的交流电转换为小家电可以接受的直流电而存在的,后者是为了在出国旅行(或者买了水货产品)的时候让本来不能直接使用当地的插座(不同地区的插座标准和电压标准不同)能接上而存在的。
所以说这并不是计算机领域生造的一个概念,在现实世界中早就存在,适配器的用途是在不改变某一个或多个产品的前提下,通过使用适配器作为“中间媒介”来进行“适配”,从而让多个本来规格、接口不同而不能直接相连的产品可以进行连接,就像其名称中的“适配”,其主要功能就是“适配”。
适配器
如果稍微了解一下二战历史,或者航母发展史应该会知道,航母的早期和雏形阶段是不存在专门设计和建造的航空母舰的,大多都是商船进行改造,以进行飞机起飞和回收测试,直到日本开工建造了第一艘专门的航空母舰(好像是凤翔号?),但其实直到二战结束,将现有军舰改造为航母的事也是屡见不鲜,不说日本中途岛惨败后为了补充航母紧急改建的一批,美帝那边也是用巡洋舰的舰体一口气造了80多艘护航航母。
我们现在来研究一下如何改造航母。
这和“如何将大象放进冰箱”的问题一样,并不困难,无非就是拆除舰炮,铺设甲板,再加上二战时候那个旋翼老爷机的皮糙血厚随便折腾的特性,还不是分分钟的事。
Cruiser
和Carrier
抽象基类都很简单,这里不做展示,关键的适配器CarrierAdapter
的代码如下:
#######################################################
#
# CarrierAdapter.py
# Python implementation of the Class CarrierAdapter
# Generated by Enterprise Architect
# Created on: 30-6��-2021 15:38:36
# Original author: 70748
#
#######################################################
from carrier_v1.src.Cruiser import Cruiser
from carrier_v1.src.Carrier import Carrier
class CarrierAdapter(Carrier):
def __init__(self, cruiser: Cruiser) -> None:
super().__init__()
self.__cruiser = cruiser
def recoveryAircraft(self):
print("{!s} recovering aircraft".format(self.__cruiser))
def takeOffAircraft(self):
print("{!s} taking off aircraft".format(self.__cruiser))
这里的关键在于其构造函数接收了一个Cruiser
实例作为引用,并且利用这个引用实现了Carrier
的相关抽象方法。利用这个“航母-巡洋舰”适配器我们就可以轻松地将Cruiser
的子类“改造”为Carrier
:
from .Cruiser import Cruiser
class ClevelandCruiser(Cruiser):
def gunFire(self):
print("{!r} starting gun fire".format(self))
def __repr__(self) -> str:
return self.__class__.__name__
def __str__(self) -> str:
return self.__class__.__name__
这是一个“克利夫兰级”巡洋舰(事实上二战中正是该级别巡洋舰改建为了独立级护航航母),我们将其改建为航母:
import os
import sys
parentDir = os.path.dirname(__file__)+"\\.."
sys.path.append(parentDir)
from carrier_v1.src.ClevelandCruiser import ClevelandCruiser
from carrier_v1.src.CarrierAdapter import CarrierAdapter
clevelandCruiser = ClevelandCruiser()
independenceCarrier = CarrierAdapter(clevelandCruiser)
independenceCarrier.takeOffAircraft()
independenceCarrier.recoveryAircraft()
clevelandCruiser.gunFire()
# ClevelandCruiser taking off aircraft
# ClevelandCruiser recovering aircraft
# ClevelandCruiser starting gun fire
可以看到,通过independenceCarrier = CarrierAdapter(clevelandCruiser)
我们利用适配器将克利夫兰级巡洋舰改装为了独立级航母,并且改建的航母可以像正常航母那样释放和回收飞机。
当然和现实中不同的是在我们的代码世界原有的巡洋舰依然存在,并且可以正常进行舰炮开火:clevelandCruiser.gunFire()
,这与其说是改建战舰,不如说是高达世界里的某种外挂增强装甲,可以随时剥离装甲使用原有机体进行战斗。
无论如何,这里展示的在没有改变原有产品(Carrier
和Cruiser
)的情况下,通过创建适配器(CarrierAdapter
)讲原有产品结合了起来,并可以进行使用。
上面示例的完整代码见Github仓库carrier_v1。
适配器模式的标准UML如下:
下面对UML中的类进行分别说明:
Client
代表使用适配器模式的客户端程序,在上面的示例中就是一段测试用代码。Target
表示需要适配为的最终产品形态,因为依赖倒置原则,以及Target
要用于约束适配器的对外“接口”,所以Target
一般会是抽象基类或者接口。Adaptee
表示被适配的对象的类,可以是具体的类或者抽象类,视情况而定。Adapter
表示适配器类,主要用于将一个Adaptee
对象“适配”为Target
。
类适配器
如果有人了解苏联海军的航母发展的话,就会知道在上世纪70年代左右苏联红海军发展航母的时候非常拧巴,一方面是面对北大西洋北约的围堵局面,迫切需要一款能够远洋作战帮助夺取制空权,或者至少提供舰队放空作用的军舰,另一方面苏联中央一直认为航空母舰是一种资本主义国家的进攻性武器而不予批准建造,结果就是苏联相关舰船设计局“创新性”提出了一种载机巡洋舰的设计。
长这个样子:
虽然说现在看起来很是歪门邪道,但是其实是满足苏联的突破战略的,要知道苏联在当时是无论如何都无法和美帝在海上打一场持久的海空争夺战的,但是凭借以载机巡洋舰为核心的舰队,可以在提供有限防空的同时向对面的舰队倾斜日炙或花岗岩重型反舰导弹,进行饱和打击,这是唯一的出路。
当然,红色帝国也不是没有考虑正经的航母,就算是嘴上说着“资本主义的邪恶进攻武器”,但是身体还是挺诚实的,比如那艘永远不会建成的乌里扬诺夫号核动力航母。
扯的有点远,我们分析一下苏联特色的载机巡洋舰,这级军舰同时具有导弹巡洋舰和航母的特点,既可以发射重型反舰导弹,也可以起降雅克系列垂直舰载机,虽然可以说啥都行,但啥都不行,反舰作战不如俄海军现在的彼得大帝等核动力巡洋舰,舰载机不如搭载米格或苏霍伊系列的后来的瓦良格级,但有总比没有强,至少当时的红海军是有了这么一型战舰不是。
我们现在从适配器模式的角度分析一下这款战舰的特点:
可以看到,载机巡洋舰AircraftCruiser
通过从导弹巡洋舰MissileCruiser
和航母Carrier
多继承,从而具有了两种舰船的特点,通过这种方式创建的适配器我们称呼为类适配器,前面所说的适配器更准确的应该称之为对象适配器。
这个示例相对简单,就不具体用代码实现了,感兴趣的可以自行编写。
类适配器与对象适配器不同,是通过继承而非组合实现两个或以上的不同类型的“适配”。
需要注意的是,虽然这里MissileCruiser
和Carrier
都是抽象基类(这是考虑到直接继承自两种现成舰船不太合适),但实际使用中类适配器需要适配的必然是可以实例化的非抽象类(对抽象类适配并无实际意义)。
类适配器 VS 对象适配器
- 类适配器通过多继承实现,如果是不支持多继承的编程语言(如Java)则无法实现,对象适配器通过组合实现,并无此限制。
- 对象适配器可以对遵循同一抽象基类或接口的类及其子类进行适配,类适配器只能适配其派生的直接基类。
- 对象适配器更符合多用组合少用继承的设计原则,更具有“弹性”,而类适配器不具有这些特点。
- 对象适配器不需要知道被适配对象内部的处理逻辑,只需要利用其对外公开的方法实现所适配的目标接口。类适配器因为直接从被适配类型派生,继承了其所有处理逻辑,因此可能不需要进行任何修改即可使用,但也可能需要进行一定修改,甚至是需要处理多继承带来的某些同名方法覆盖的问题,但无论如何都需要处理并厘清基类的内部逻辑。
外观模式
有时候我们需要对一个已有的复杂系统作出封装,以方便使用。
比如Linux的各种桌面,实际上就是对Linux内核的封装,让用户通过简单的图形界面可以进行各种WIFI或者其他的系统配置修改工作。
这种思路看起来很是简单和自然,但也是一种设计模式,称之为外观模式。
我们在上一篇设计模式 with Python 6:命令模式中使用一个智能家居系统作为举例,其中用SmartHomeControl
表示智能家居的核心控制面板,对接入的智能家居进行汇总展示并控制,除此之外,我们完全可以单独构建一个SimpleSmartHome
,实现一些更“傻瓜式”的一键功能,比如“一键看电影”:
当然,这里我们也可以用命令模式以及宏命令来实现封装,只不过这两者的封装层次不同,命令模式是将不同智能家居中端封装为了统一的操作(命令),然后可以用宏命令来组装“组合命令”,而外观模式相对更简单,只是在所有智能家居之上创建了一个可以“简易操作”的统一外部接口。
- 这里的
Raspberry
指的是一个流行的微电脑“树莓派”,目前我就是在用它上边安装的媒体中心应用kodi来观看直播和电影。- 树莓派使用的是Linux发行版系统,
Service
指代其上运行的服务进程。
大部分代码都很简单,这里仅展示作为外观模式的SimpleSmartHome
:
#######################################################
#
# SimpleSmartHome.py
# Python implementation of the Class SimpleSmartHome
# Generated by Enterprise Architect
# Created on: 01-7��-2021 11:07:50
# Original author: 70748
#
#######################################################
from simple_smart_home.src.AirConditioner import AirConditioner
from .Light import Light
from .SmartTV import SmartTV
from .Raspberry import Raspberry
class SimpleSmartHome:
def watchMovie(self):
conditioner = AirConditioner()
conditioner.on()
conditioner.setTemprature(18)
ligt = Light()
ligt.setLightness(10)
tv = SmartTV()
tv.on()
rasp = Raspberry()
rasp.start()
rasp.startService("Kodi")
rasp.exec("Kodi", "playFavorateMovie")
完整代码见Github仓库simple_smart_home。
最少知识原则
事实上,在上边的Simple_smart_home
中,其实是可以通过rasp.startService("Kodi")
返回一个具体服务,然后直接在SimpleSmartHome
中进行调用kodi.playFavorateMovie()
,亦或者可以直接使用rasp.startService("Kodi").playFavorateMovie()
的写法。
但是这样一来,SimpleSmartHome
将与Kodi
这个具体服务产生依赖关系。
在设计模式中,存在一个最少知识原则。
这个原则说的是,当前类或者函数,只应当调用其持有的引用、传入的参数、在局部创建的变量的相应方法。
这个原则的反面就是像上面那样rasp.startService("Kodi").playFavorateMovie()
,这种调用了局部变量方法返回值的方法的方式是违反该原则的,造成了可以避免的多余依赖。
当然该原则并非是强制性的,所有的设计模式原则都是非强制性的,它们只代表了一般性的最优方式,而并非所有情况。
就像上面的最少知识原则,要实现该原则往往不得不创建额外的代码(如Raspberry
中的exec()
方法)来避免方法的级联调用。所以我们需要评估使用该原则带来的好处和为了该原则将增加的额外代码量的坏处。
总结
总的来说,适配器模式是一个相当简单的模式,而且功能非常单一。但是它和之前介绍过的装饰器模式颇为类似,所以这里对这两种模式以及外观模式进行总结。
- 装饰器模式的目的在于在不改变“外观”(统一的抽象类、接口)的情况下增强其能力。
- 适配器模式往往需要为了将原本不能协同工作的两个或以上类型进行“适配”,所以主要关注于如何在不同的抽象类、接口之间进行转换。
- 外观模式的目的则是为了对于复杂的系统提供一个简化易用的接口,从而提供一个统一的易用的抽象层。
关于适配器模式的内容介绍完毕,谢谢阅读。