Python設計模式之適配器模式

適配器模式

結構型設計模式處理一個系統中不同實體(比如,類和對象)之間的關系,關注的是提供一 種簡單的對象組合方式來創造新功能(請參考[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中我們可以沿襲傳統方式使用子類(繼承)來實現適配器模式,但這種技術是一種很棒的替代方案。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容