見出し画像

DP.07:外部コードをラップしてIFを統一する。-Adapter-【Python】

【1】Adapterパターン概要

Adapterパターンは「何らかの理由で変更が難しいプログラム(*)側を変更せずに、新たに用意したオブジェクト(Adapterオブジェクト)経由でプログラムをコールする書き方」。
(※例えば、レガシーコードや他の連動システムにも修正影響が出るモジュール、あるいはインターフェースを改変できないライブラリなど)

■イメージ図(メディアプレイヤーにAdapterオブジェクトをかませる)

画像1

【2】例:メディアプレーヤー

例としてメディアプレイヤーを作成することを考えてみる。

<作成における制約事項>
・「.mp3」などのメディアファイルを再生するオブジェクトは新規作成する。
・一方、Blu-RayやHDDVDを再生できるモジュールはあるのだが、何らかの事情によりコードを変更できないものとする。

Adapterパターンを利用することで、どのモジュールを使う場合でも「play()」という名前のメソッドで再生する動作を起動するようにしたい。

■イメージ図

画像2

■コードイメージ

obj1 = MediaFilePlayer('abcdefg.mp3')
obj1.play()


obj2 = BluRayPlayer()
... Adapterオブジェクトをかませる処理をいれる ...
obj2.play() # play()というメソッド名でコールさせる


obj3 = HDDVDPlayer()
... Adapterオブジェクトをかませる処理をいれる ...
obj3.play() # play()というメソッド名でコールさせる

▲どのオブジェクトであっても「play()」という名前で実行させる

【3】Adapterクラスを用意する

■新たに作成したクラスオブジェクト(Adapter不要)

# 新たに用意したクラス
class MediaFilePlayer:
   def __init__(self, name=""):
       self.name = name
   
   def __str__(self):
       return f'data file is {self.name}'
   
   # 再生は「play」という名前のメソッド
   def play(self):
       return 'Play from Music Player !'

このオブジェクトは、そのまま通常通りインスタンスを生成すれば「play()」メソッドをコールできるためAdapterは不要。

obj1 = MediaFilePlayer('abcdefg.mp3')
print(obj1.play())

# 出力例
Play from Music Player !


一方で何らかの理由で変更ができないモジュールについて。
■external.py (※外部ファイルとして用意)

# 例:
# 何らかの事情により手が加えられないオープンソースやレガシーコードなど
# 「再生」に関するメソッド名がバラバラの状態

class BluRayPlayer:
   def __init__(self, name=""):
       self.name = name
   
   def __str__(self):
       return f'the Blu-Ray Player mode'
   
   def start_bluray(self):
       return 'start from Blu-Ray Player '


class HDDVDPlayer:
   def __init__(self,name=""):
       self.name = name
   
   def __str__(self):
       return f'title is {self.name}'
   
   def play_stream(self):
       return 'start HD-DVD data'

このままでは「メディアを再生するメソッド」がバラバラ(start_bluray()、play_stream())であり、統一したメソッド名「play()」でコールできない。

obj2 = BluRayPlayer()
obj2.play() # コールできない


obj3 = HDDVDPlayer()
obj3.play() # コールできない

そこでAdapterクラスを用意してラッピングする。

# Adaperクラス
class Adapter:
   def __init__(self, obj, adapted_methods):
       
       self.obj = obj # 外部のモジュールと紐づける
       
       print(self.__dict__) # 属性更新「前」を出力

       # setattrで属性追加
       for k,v in adapted_methods.items():
           setattr(self,k,v)
       
       print(self.__dict__) # 属性更新「後」を出力
   
   def __str__(self):
       return str(self.obj)

■Adapterクラスの使用例

# 例:何らかの事情で手が加えられない外部モジュール
from external import  BluRayPlayer, HDDVDPlayer


class Adapter:
  ... 省略 ...


# Adapterクラスを使って外部モジュールをラップする
obj = BluRayPlayer()
adapted_methods = dict(play=obj.start_bluray) # Adapterクラスに追加する属性
obj = Adapter(obj, adapted_methods)

obj.play() # play()メソッドでコール可能

▲Adapterクラスを経由することでインターフェイスを「play()」に統一することができた。
ざっくり言うと、「Adapterクラス」に「play」という「attribute(属性)」を後から追加している、ということ。

【4】補足:クラスのattribute(属性)追加について

Adapterパターンを実現するにあたり、今回、後からクラスにattribute(属性:クラス変数やクラスメソッド相当)を追加する仕組みを利用した。
ここでは「setattr()」を使ってオブジェクトにattribute(属性:メソッド相当)を追加した。

■setattr

pythonでは後からクラスにattribute(属性)が追加できる。以下は簡単な動作検証。

■動作確認

# 例:何らかの事情で手が加えられない外部モジュール
from external import  BluRayPlayer, HDDVDPlayer

# Adaperクラス
class Adapter:
   def __init__(self, obj, adapted_methods):
       
       self.obj = obj
       
       print(self.__dict__) # 属性更新「前」
       
       #setattrで設定
       for k,v in adapted_methods.items():
           print(k, v) #追加する属性を出力
           setattr(self,k,v)
       
       print(self.__dict__) # 属性更新「後」
   
   def __str__(self):
       return str(self.obj)


obj = BluRayPlayer()
adapted_methods = dict(play=obj.start_bluray) # Adapterクラスに追加する属性
obj = Adapter(obj, adapted_methods)

# 実行結果例
{'obj': <external.BluRayPlayer object at 0x0000024274012FD0>}

play <bound method BluRayPlayer.start_bluray of <external.BluRayPlayer object at 0x0000024274012FD0>>

{'obj': <external.BluRayPlayer object at 0x0000024274012FD0>, 'play': <bound method BluRayPlayer.start_bluray of <external.BluRayPlayer object at 0x0000024274012FD0>>}

画像3

▲用意した「attribute(属性)」が追加されているのがわかる

※おまけ
現在のクラスに登録されている「attribute(属性)」を出力するために「特殊attribute」の「object.__dict__」を使用した。

「setattr()」以外にも「object.__dict__.update()」で更新させることも可能。

# Adaperクラス
class Adapter:
   def __init__(self, obj, adapted_methods):
       
       self.obj = obj
       
       print(self.__dict__) # 属性更新「前」
       
       print(adapted_methods)
       # 「object.__dict__.update()」でattributeを更新
       self.__dict__.update(adapted_methods)
       
       #setattrで設定
       #for k,v in adapted_methods.items():
       #    print(k, v) #追加する属性を出力
       #    setattr(self,k,v)
       
       print(self.__dict__) # 属性更新「後」
   
   def __str__(self):
       return str(self.obj)

【5】全体コード

長々と書いたが、ようはAdapterオブジェクトに、

・手を加えられない対象のオブジェクトを紐づける。
・「attribute(属性)」を追加して、対象オブジェクトのインターフェースにする。

という感じ。

■external.py

# 例:
# 何らかの事情により手が加えられないオープンソースやレガシーコードなど
# 「再生」に関するメソッド名がバラバラ

class BluRayPlayer:
   def __init__(self, name=""):
       self.name = name
   
   def __str__(self):
       return f'the Blu-Ray Player mode'
   
   def start_bluray(self):
       return 'start from Blu-Ray Player '


class HDDVDPlayer:
   def __init__(self,name=""):
       self.name = name
   
   def __str__(self):
       return f'title is {self.name}'
   
   def play_stream(self):
       return 'start HD-DVD data'

■main.py

# 例:何らかの事情で手が加えられない外部モジュール
from external import  BluRayPlayer, HDDVDPlayer
import sys


# 新たに用意したクラス(Adapter不要)
class MediaFilePlayer:
   def __init__(self, name=""):
       self.name = name
   
   def __str__(self):
       return f'data file is {self.name}'
   
   # 再生は「play」という名前のメソッド
   def play(self):
       return 'Play from Music Player !'


# Adaperクラス
class Adapter:
   def __init__(self, obj, adapted_methods):
       
       self.obj = obj # 対象オブジェクトの紐づけ
       
       print(self.__dict__) # 属性更新「前」

       #  adapted_methodsに記載の内容で
       # 引数として渡された「objクラスの__dict__」の内容を更新する
       #self.__dict__.update(adapted_methods)
       
       # setattrで設定
       for k,v in adapted_methods.items():
           #print(k, v)
           setattr(self,k,v)
       
       print(self.__dict__) # 属性更新「後」
   
   def __str__(self):
       return str(self.obj)


def main():

   # それぞれのサンプルオブジェクト
   objects = [MediaFilePlayer('abcdefg.mp3'), BluRayPlayer(), HDDVDPlayer()]
   
   selected_number = int(input("select number [0]:MediaFile [1]:BlueRay [2]:HDDVD >> "))
   if selected_number not in [0,1,2]:
       print(f"wrong number!! :{selected_number} ")
       sys.exit(0)

   
   obj = objects[selected_number]
   if selected_number > 0:

       if selected_number == 1:
           adapted_methods = dict(play=obj.start_bluray)
       elif selected_number == 2:
           adapted_methods = dict(play=obj.play_stream)
       # else:
       #     adapted_methods = dict(play=obj.play) # 新しいモジュールはアダプタ不要

       obj = Adapter(obj, adapted_methods)

   
   print(f'{obj} {obj.play()}')
   print("----")



if __name__ == "__main__":
   main()

#実行結果

select number [0]:MediaFile [1]:BlueRay [2]:HDDVD >> 0
data file is abcdefg.mp3 Play from Music Player !
----


select number [0]:MediaFile [1]:BlueRay [2]:HDDVD >> 1
{'obj': <external.BluRayPlayer object at 0x00000241708D8D60>}
{'obj': <external.BluRayPlayer object at 0x00000241708D8D60>, 'play': <bound method BluRayPlayer.start_bluray of <external.BluRayPlayer object at 0x00000241708D8D60>>}
the Blu-Ray Player mode start from Blu-Ray Player
----


select number [0]:MediaFile [1]:BlueRay [2]:HDDVD >> 2
{'obj': <external.HDDVDPlayer object at 0x000001DFAFD77A90>}
{'obj': <external.HDDVDPlayer object at 0x000001DFAFD77A90>, 'play': <bound method HDDVDPlayer.play_stream of <external.HDDVDPlayer object at 0x000001DFAFD77A90>>}
title is start HD-DVD data
----

画像4


もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。