
DP.07:外部コードをラップしてIFを統一する。-Adapter-【Python】
【1】Adapterパターン概要
Adapterパターンは「何らかの理由で変更が難しいプログラム(*)側を変更せずに、新たに用意したオブジェクト(Adapterオブジェクト)経由でプログラムをコールする書き方」。
(※例えば、レガシーコードや他の連動システムにも修正影響が出るモジュール、あるいはインターフェースを改変できないライブラリなど)
■イメージ図(メディアプレイヤーにAdapterオブジェクトをかませる)

【2】例:メディアプレーヤー
例としてメディアプレイヤーを作成することを考えてみる。
<作成における制約事項>
・「.mp3」などのメディアファイルを再生するオブジェクトは新規作成する。
・一方、Blu-RayやHDDVDを再生できるモジュールはあるのだが、何らかの事情によりコードを変更できないものとする。
Adapterパターンを利用することで、どのモジュールを使う場合でも「play()」という名前のメソッドで再生する動作を起動するようにしたい。
■イメージ図

■コードイメージ
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>>}

▲用意した「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
----

いいなと思ったら応援しよう!
