適配器模式
結構型設計模式處理一個系統中不同實體(比如,類和對象)之間的關系,關注的是提供一 種簡單的對象組合方式來創造新功能(請參考[GOF95,第155頁]和網頁[t.cn/RqBdWzD])。
適配器模式(Adapter pattern)是一種結構型設計模式,幫助我們實現兩個不兼容接口之間的兼容。首先,解釋一下不兼容接口的真正含義。如果我們希望把一個老組件用于一個新系統中,或者把一個新組件用于一個老系統中,不對代碼進行任何修改兩者就能夠通信的情況很少見。但又并非總是能修改代碼,或因為我們無法訪問這些代碼(例如,組件以外部庫的方式提供),或因為修改代碼本身就不切實際。在這些情況下,我們 可以編寫一個額外的代碼層,該代碼層包含讓兩個接口之間能夠通信需要進行的所有修改。這個代碼層就叫適配器。
電子商務系統是這方面眾所周知的例子。假設我們使用的一個電子商務系統中包含一個calculate_total(order)函數。這個函數計算一個訂單的總金額,但貨幣單位為丹麥克朗(Danish Kroner,DKK)。顧客讓我們支持更多的流行貨幣,比如美元(United States Dollar,USD) 和歐元(Euro,EUR),這是很合理的要求。如果我們擁有系統的源代碼,那么可以擴展系統,方法是添加一些新函數,將金額從DKK轉換成USD,或者從DKK轉換成EUR。但是如果應用僅以外部庫的方式提供,我們無法訪問其源代碼,那又該怎么辦呢?在這種情況下,我們仍然可以使用這個外部庫(例如,調用它的方法),但無法修改/擴展它。解決方案是編寫一個包裝器(又名適配器)將數據從給定的DKK格式轉換成期望的USD或EUR格式。
適配器模式并不僅僅對數據轉換有用。通常來說,如果你想使用一個接口,期望它是function_a(),但僅有function_b()可用,那么可以使用一個適配器把function_b()轉換(適配)成function_a()(請參考[Eckel08,第207頁]和網頁[t.cn/RqBdTCD])。不僅對于函數可以這樣做,對于函數參數也可以如此。其中一個例子是,有一個函數要求參數x、y、z,但你手頭只有一個帶參數x、y的函數。在4.4節我們將看到如何使用適配器模式。
#下面是一個來自github的示例:
# http://ginstrom.com/scribbles/2008/11/06/generic-adapter-class-in-python/
import os
class Dog(object):
def __init__(self):
self.name = "Dog"
def bark(self):
return "woof!"
class Cat(object):
def __init__(self):
self.name = "Cat"
def meow(self):
return "meow!"
class Human(object):
def __init__(self):
self.name = "Human"
def speak(self):
return "'hello'"
class Car(object):
def __init__(self):
self.name = "Car"
def make_noise(self, octane_level):
return "vroom%s" % ("!" * octane_level)
class Adapter(object):
"""
Adapts an object by replacing methods.
Usage:
dog = Dog
dog = Adapter(dog, dict(make_noise=dog.bark))
"""
def __init__(self, obj, adapted_methods):
"""We set the adapted methods in the object's dict"""
self.obj = obj
self.__dict__.update(adapted_methods)
def __getattr__(self, attr):
"""All non-adapted calls are passed to the object"""
return getattr(self.obj, attr)
def main():
objects = []
dog = Dog()
objects.append(Adapter(dog, dict(make_noise=dog.bark)))
cat = Cat()
objects.append(Adapter(cat, dict(make_noise=cat.meow)))
human = Human()
objects.append(Adapter(human, dict(make_noise=human.speak)))
car = Car()
car_noise = lambda: car.make_noise(3)
objects.append(Adapter(car, dict(make_noise=car_noise)))
for obj in objects:
print("A", obj.name, "goes", obj.make_noise())
if __name__ == "__main__":
main()
A Dog goes woof!
A Cat goes meow!
A Human goes 'hello'
A Car goes vroom!!!
現實生活的例子
也許我們所有人每天都在使用適配器模式,只不過是硬件上的,而不是軟件上的。如果你有一部智能手機或者一臺平板電腦,在想把它(比如,iPhone手機的閃電接口)連接到你的電腦時,就需要使用一個USB適配器。如果你從大多數歐洲國家到英國旅行,在為你的筆記本電腦充電時,需要使用一個插頭適配器。如果你從歐洲到美國旅行,同樣如此;反之亦然。適配器無處不在!
下圖展示了硬件適配器的若干例子(請參考網頁[t.cn/RqBdTCD]),經sourcemaking.com允許使用。
軟件的例子
Grok是一個Python框架,運行在Zope 3之上,專注于敏捷開發。Grok框架使用適配器,讓已有對象無需變更就能符合指定API的標準(請參考網頁[t.cn/RqBd1gM])。
Python第三方包Traits也使用了適配器模式,將沒有實現某個指定接口(或一組接口)的對象轉換成實現了接口的對象(請參考網頁[t.cn/RqBdg28])。
應用案例
在某個產品制造出來之后,需要應對新的需求之時,如果希望其仍然有效,則可以使用適配器模式(請參考網頁[t.cn/RqBdTCD])。通常兩個不兼容接口中的一個是他方的或者是老舊的。如果一個接口是他方的,就意味著我們無法訪問其源代碼。如果是老舊的,那么對其重構通常是不切實際的。更進一步,我們可以說修改一個老舊組件的實現以滿足我們的需求,不僅是不切實際的,而且也違反了開放/封閉原則(請參考網頁[t.cn/RqBdFAO])。
開放/封閉原則(open/close principle)是面向對象設計的基本原則之一(SOLID中的O),聲明一個軟件實體應該對擴展是開放的,對修改則是封閉的。本質上這意味著我們應該無需修改一個軟件實體的源代碼就能擴展其行為。適配器模式遵從開放/封閉原則(請參考網頁[t.cn/RqBghbH])。
因此,在某個產品制造出來之后,需要應對新的需求之時,如果希望其仍然有效,使用適配器是一種更好的方式,原因如下所示。
- 不要求訪問他方接口的源代碼
- 不違反開放/封閉原則
實現
使用Python實現適配器設計模式有多種方法(請參考[Eckel08,第207頁]。Bruce Eckel演示的所有技巧都是使用繼承,但是Python提供了一種替代方案,在我看來,這種實現適配器的方式更地道一些。你應該已熟悉這一替代技巧,因為它使用了類的內部字典,在第3章中我們就看到了如何使用類的內部字典。
先來看看“我們有什么”部分。我們的應用有一個Computer類,用來顯示一臺計算機的基本信息。這一例子中的所有類,包括Computer類,都非常簡單,因為我們希望關注適配器模式,而不是如何盡可能完善一個類。
class Computer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} computer'.format(self.name)
def execute(self):
return 'executes a program'
在這里,execute方法是計算機可以執行的主要動作。這一方法由客戶端代碼調用。
現在再來看看“我們想要什么”部分。我們決定用更多功能來豐富應用,并且幸運地在兩個與我們應用無關的代碼庫中發現兩個有意思的類,Synthesizer和Human。在Synthesizer類中,主要動作由play()方法執行。在Human類中,主要動作由speak()方法執行。為表明這兩個類是外部的,將它們放在一個單獨的模塊中,如下所示。
class Synthesizer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} synthesizer'.format(self.name)
def play(self):
return 'is playing an electronic song'
class Human:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} Human'.format(self.name)
def play(self):
return 'say hello'
看起來不錯,但是有一個問題:客戶端僅知道如何調用execute()方法,并不知道play()和speak()。在不改變Synthesizer和Human類的前提下,我們該如何做才能讓代碼有效?適配器是救星!我們創建一個通用的Adapter類,將一些帶不同接口的對象適配到一個統一接口中。_init_()方法的obj參數是我們想要適配的對象,adapted_methods是一個字典,鍵值對中的鍵是客戶端要調用的方法,值是應該被調用的方法。
class Adapter:
def __init__(self, obj, adapted_methods):
self.obj = obj
self.__dict__.update(adapted_methods)
def __str__(self):
return str(self.obj)
下面看看使用適配器模式的方法。列表objects容納著所有對象。屬于Computer類的可兼容對象不需要適配??梢灾苯訉⑺鼈兲砑拥搅斜碇小2患嫒莸膶ο髣t不能直接添加。使用Adapter類來適配它們。結果是,對于所有對象,客戶端代碼都可以始終調用已知的execute()方法,而無需關心被使用的類之間的任何接口差別。
def main():
objects = [Computer('Asus')]
synth = Synthesizer('moog')
objects.append(Adapter(synth, dict(execute=synth.play)))
human = Human('Bob')
objects.append(Adapter(human, dict(execute=human.speak)))
for i in objects:
print('{} {}'.format(str(i), i.execute()))
現在來看看適配器模式例子的完整代碼(文件external.py和adapter.py),如下所示。
class Synthesizer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} synthesizer'.format(self.name)
def play(self):
return 'is playing an electronic song'
class Human:
def __init__(self, name):
self.name = name
def __str__(self):
return '{} the human'.format(self.name)
def speak(self):
return 'says hello'
class Computer:
def __init__(self, name):
self.name = name
def __str__(self):
return 'the {} computer'.format(self.name)
def execute(self):
return 'executes a program'
class Adapter:
def __init__(self, obj, adapted_methods):
self.obj = obj
self.__dict__.update(adapted_methods)
def __str__(self):
return str(self.obj)
objects = [Computer('Asus')]
synth = Synthesizer('moog')
objects.append(Adapter(synth, dict(execute=synth.play)))
human = Human('Bob')
objects.append(Adapter(human, dict(execute=human.speak)))
for i in objects:
print('{} {}'.format(str(i), i.execute()))
the Asus computer executes a program
the moog synthesizer is playing an electronic song
Bob the human says hello
我們設法使得Human和Synthesizer類與客戶端所期望的接口兼容,且無需改變它們的源代碼。這太棒了!
這里有一個為你準備的挑戰性練習,當前的實現有一個問題,當所有類都有一個屬性name時,以下代碼會運行失敗。
for i in objects:
print(i.name)
首先想想這段代碼為什么會失敗?雖然從編碼的角度來看這是有意義的,但對于客戶端代碼來說毫無意義,客戶端不應該關心“適配了什么”和“什么沒有被適配”這類細節。我們只是想提供一個統一的接口。該如何做才能讓這段代碼生效?
思考一下如何將未適配部分委托給包含在適配器類中的對象。
小結
本章介紹了適配器設計模式。我們使用適配器模式讓兩個(或多個)不兼容接口兼容。為了引起讀者的興趣,本章先提到一個應該支持多種貨幣的電子商務系統1。我們每天都在為設備的互連、充電等使用適配器。
適配器讓一件產品在制造出來之后需要應對新需求之時還能工作。Python框架Grok和第三方包Traits各自都使用了適配器模式來獲得API一致性和接口兼容性。開放/封閉原則與這些方面密切相關。
我們看到了如何使用適配器模式,無需修改不兼容模型的源代碼就能獲得接口的一致性。這是通過讓一個通用的適配器類完成相關工作而實現的。雖然在Python中我們可以沿襲傳統方式使用子類(繼承)來實現適配器模式,但這種技術是一種很棒的替代方案。