适配器模式被设计用于与已有代码进行交互。适配器适用于允许两个已存在的对象在一起工作,即使他们的接口不兼容。就像允许USB键盘出入PS/2端口一样,适配对象位于两个不同接口之间,对两个接口进行即时转换。适配器对象的唯一工作就是执行转换,转换可能涉及很多任务,比如将参数转换成另一种格式,重新排列参数的顺序,调用名称不同的方法或提供默认参数。
在结构上,适配器模式类似于一个简化的装饰器模式。装饰器模式通常提供它们所替代的相同接口,而适配器模式在两个不同的接口之间进行匹配。下图是适配器在UML中的表现形式:这里,接口1期望调用一个称为make_action(some, args)的方法。我们已经有个很完美的接口2能做到我们想要的一切(并且为了避免重复,我不想重写他们),但是提供的方法被命名为diffe_action(other, args)。适配器类将会实现make_action接口并将参数匹配到已有的接口。
这里的优势在于,代码一次性的对一个接口到其他所有借口进行了全部匹配。另一种方法则使得我们每次访问这个代码时,都需要在多个不同的地方对其进行直接转换。
适配器模式例子
假设我们有以下的类,以格式'YYYY-MM-DD'接收一个字符串的日期并通过日期计算一个人的年龄:
class AgeCaculator(object):
def __init__(self, birthday):
self.year, self.month, self.day = (int(x) for x in birthday.split('-'))
def calculate_age(self, date):
year, month, day = (int(x) for x in data.split('-'))
age = year - self.year
if (month, day) < (self.month, self.day):
age -= 1
return age
这是一个非常简单的类,可以一眼看出他在做什么。但是,我们必须考虑一下程序员的想法,这里使用了一个特定格式的字符串,而不是python中十分有用的内置datetime
库。我们编写的大多数程序都将要与datetime
对象,而不是字符串交互。
我们有多种方法来解决这种问题,我们可以重写这个类来接受datetime
对象,这将可能会更加准确。但是,如果这个类是由第三方提供的,我们不知道他的内部是什么,或者我们根本就不允许去更改他们,我们就需要尝试一下别的东西。我们可以按照他原本的模样来使用这个类,每当我们要通过datetime.date
对象来计算年龄时,我们可以调用datetime.date.strftime('%Y-%M-%d')
函数将其转换为适当的格式。但是转换发生在很多地方,那么我们就需要在那么多地方都进行更改,而且一旦发生错误,我们还需要一个个的改回去。这是非常难以进行维护的,他违反了DRY(Do Not Repeat Yourself)原则。
或者,我们可以编写一个类适配器,运行正常的日期插入到一个普通的AgeCalculator类中:
import datetime
class DateAgeAdapter(object):
def _str_date(self, date):
return date.strftime("%Y-%m-%d")
def __init__(self, birthday):
birthday = self._str_date(birthday)
self.calculator = AgeCaculator(birthday)
def get_age(self, date):
date = self._str_date(date)
return self.calculator.calculate_age(date)
该适配器将datetime.date
和datetime.time
(他们对函数strftime有相同的接口)装换成我们普通的AgeCalculator可以使用的字符串。现在我们可以在新接口上使用原来的代码。我们将方法名改成了get_age
以证明调用接口也可能在寻找不同的方法名,而不仅仅是一个不同类型的参数。
创建一个类作为适配器是实现一种模式的常用方式,但是,像往常一样,还有许多其他的方法可以做到这一点。继承和多重继承可以用于将功能添加到一个类中。例如,我们可以从date类中添加适配器,这样他的工作原理就与原来的AgeCaculator相同。
import datetime
class AgeableDate(datetime.date):
def split(self, char):
return self.year, self.month, self.day
在这里我们创建了一个继承自datetime.date
的类,并添加了一个split方法,该方法接收一个参数(我们将其忽略掉),并返回一个年月日的元组。这与我们的AgeCalculator能够完美结合,因为该代码是一种特殊的格式调用strip函数,而在这种情况下,strip会返回一个年月日的元组。这个AgeCalculator只在意strip函数是否存在,并返回可接收的值;它不在意我们是否真的在传递一个字符串。但他确实有效。
bd = AgeableDate(1975, 6, 14)
bd
today = AgeableDate.today()
today
a = AgeCaculator(bd)
a.calculate_age(today)
# 输出:
AgeableDate(1975, 6, 14)
AgeableDate(2019, 4, 14)
43
但是这个使用继承替代的适配器常常会难以理解和维护。而使用适配器来替代继承,目的则会更加的明显。
我们也可以将函数当成适配器,这并不完全符合标准的适配器模式,但在通常情况下,你可以简单的传递数据到一个函数,然后将它以一个适当的形式返回作为进入进入另一个接口的入口。
参考:
《Python3 面向对象编程》