ログインボックスを表示します

検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Adapter Pattern - Part. 4 「サンプル問題」の巻 投稿一覧へ戻る

Published 2022年6月1日21:05 by T.Tsuyoshi

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Sample Problem
(サンプル問題)

求めているインターフェースが手元にないときにはアダプターが必要、という話をしました:
class WhatIHave:
"""
持っているもの
(提供されているものですから、このインターフェースを変更することはできません)
"""

def provided_function_1(self): print('Via old interface 1')
def provided_function_2(self): print('Via old interface 2')

class WhatIWant:
"""
欲しいもの
"""

def required_function(self): print('Via new interface')
このような状況でまず頭に思い浮かぶ解決策は、「欲しいもの」を「既に持っているもの」に合わせてしまおう、ということではないでしょうか。つまり上のスニペットでいえば、本当に利用したいインターフェースは WhatIWant クラスに実装されている形式のものですが、まぁ、WhatIHave は手元にあるからそのインターフェースを我慢して使おう、というようにプログラムを記述する、ということになります。
利用したい対象のオブジェクトが1つだけであればこの方法で全然問題ないでしょう、すなわち、WhatIHave オブジェクトだけを利用するのであれば。しかしもし、これに加えて2つ目のサービス、WhatIWant も実際に利用したくなったらどうでしょう。途端にインターフェースの数も2つになります。そこで、利用したいオブジェクト毎に正しいインターフェースを選択するための if 文が必要になってきます:
class Client:
def __init__(self, some_object):
self.some_object = some_object

def do_something(self):
if self.some_object.__class__ == WhatIHave:
self.some_object.provided_function_1()
self.some_object.provided_function_2()
elif self.some_object.__class__ == WhatIWant:
self.some_object.required_function()
else:
print(f'Class; {self.some_object} はサポートされていません')
システムで利用するオブジェクトがこの2種類だけであり、結果的に2種類のインターフェースを使い分ければ良いのであればこの実装でも悪くはないかもしれませんが、システム全体で利用する外部サービスが2つだけ、などという状況はほとんどないはずです。
また、プログラム内の複数の箇所で同じサービスを利用する可能性がある、というのも問題発生の原因となります。その場合、新たなインターフェースを備えたサービスの利用を開始するたびに、全ての利用箇所のコードを変更する必要が生じてしまうからです。このシリーズでも既に取り上げましたが、if 文によるサービス等の選択・追加コードをプログラム内のあちらこちらバラバラに複数記述する方法は、ほとんどの場合上手くいきません。
やりたいことは、既に手元にあるライブラリやインターフェース (WhatIHave) と実際に利用したいと考えているインターフェース (WhatIWant) の仲介役をしてくれるコードを作成することです。こうすることで、同様のサービスを追加する場合でも、そのサービス用のインターフェースをその仲介役コードに追加するだけで済み、そのサービスを実際に利用するオブジェクトとサービス本体を分離することが可能です。
Class Adapter Pattern
(クラスアダプター: 継承を利用するアダプター)
この問題への対処方法の1つは、既に持っているサービスクラス (Adaptee) と本来利用したいインターフェースを定義したクラス (Target) を継承するアダプタークラス (Adapter) を実装することです。
次の例では、利用したい外部サービス (WhatIHave) をインポートし、それが提供するインターフェースを「覆って」自分が本来利用したいと考えているインターフェース (WhatIWant) でアクセス可能にするためのアダプタークラス (MyAdapter) を定義しています。実際のサービス利用者であるオブジェクト (ClientObject) は、そのインターフェースを利用してリクエストを出すことで、外部サービスが提供する機能を「望む」インターフェースで使用できます:
class_adapter.py
# 本来外部サービスは以下のようにインポートすることになると思いますが、
# この例では全体像を見やすくするため class を記述しています
# from third_party import WhatIHave


class WhatIHave:
"""
Adaptee (持っているもの)

提供されているものですから、このインターフェースを変更することはできません
"""

def provided_function_1(self): print('Via old interface 1')
def provided_function_2(self): print('Via old interface 2')


class WhatIWant:
"""
Target (欲しいもの)

このシステムで実際に利用したいと考えているインターフェース
"""

def required_function(self): pass


class MyAdapter(WhatIHave, WhatIWant):
"""
Adapter (仲介役コード)

Adaptee / Target 双方を継承します
Adaptee のインターフェースを Taget のインターフェースでアクセス可能にします
"""

def required_function(self):
self.provided_function_1()
self.provided_function_2()


class ClientObject:
"""
Client (サービスの利用者)

このシステムに共通する望み通りのインターフェースを利用してサービスを使用できます
"""

def __init__(self):
self.adapter = MyAdapter()

def do_something(self):
self.adapter.required_function()

if __name__ == '__main__':
client = ClientObject()
client.do_something()
アダプターはシステム内のあらゆるところで活用することができます。Python においてはアダプターを適用できるのはクラスにとどまりません。呼び出し可能なものであれば、デコレータ (decorators)、クロージャ (closures)、functools ライブラリを利用することでアダプターを適用することができます。
あるインターフェースを別のインターフェースに適合させるアダプターの実装には上記例とは異なる方法もあります。そして、次に紹介するその方法の方がより Python 的 (Pythonic) であるといえます。
Object Adapter Pattern
(オブジェクトアダプター: Adaptee のオブジェクトを利用するアダプター)
クラスアダプターパターンでは、インターフェースの仲介役を務めるアダプタークラスが Adaptee と Target クラスの双方を継承 (is-a の関係) することでその役割を果たしていましたが、この方法では composition (has-a の関係) でその機能を実現します。つまり、アダプタークラスにおいて Adaptee のオブジェクトを保持し、提供する「望む」インターフェースが呼び出されたときに、その Adaptee オブジェクトのインターフェースを呼び出すことになります。この方法の方が、継承パターンよりも実装の煩雑さが軽減されます (この小規模なコードではあまり実感できませんが...):
# 本来外部サービスは以下のようにインポートすることになると思いますが、
# この例では全体像を見やすくするため class を記述しています
# from third_party import WhatIHave


class WhatIHave:
"""
Adaptee (持っているもの)

提供されているものですから、このインターフェースを変更することはできません
"""

def provided_function_1(self): print('Via old interface 1')
def provided_function_2(self): print('Via old interface 2')


class WhatIWant:
"""
Target (欲しいもの - 実際に利用したいインターフェース)
"""

def required_function(self): pass


class ObjectAdapter(WhatIWant):
"""
Adapter (仲介役コード)

Adaptee (WhatIHave) を継承するのではなくそのオブジェクトを保持します
"""

def __init__(self):
self.what_i_have = WhatIHave()

def required_function(self):
self.what_i_have.provided_function_1()

def __getattr__(self, attr):
"""
このアダプタークラスが所持しない属性へのアクセスがあった場合、
Adaptee で処理するようにします。

これによって、コンポジションでの実装に継承の機能を組み込めます。
"""

return getattr(self.what_i_have, attr)


class ClientObject:
"""
Client (サービスの利用者)
"""

def __init__(self):
self.adapter = ObjectAdapter()

def do_something(self):
self.adapter.required_function() # 'Via old interface 1' を出力します

# ObjectAdapter クラスの属性ではない属性へアクセス
self.adapter.provided_function_2() # 'Via old interface 2' を出力します

if __name__ == '__main__':
client = ClientObject()
client.do_something()
この実装方法のアダプタークラス (Adapter: ObjectAdapter) では、実際に利用したいと「望んでいる」インターフェースを定義したクラス (Target: WhatIWant) だけを継承しています。そして機能を提供してもらうクラス (Adaptee: WhatIHave) のオブジェクトをインスタンス変数 (self.what_i_have) として所持しています。
今回の実装で皆さんの興味を引くために変更を加えた点は、アダプタークラスが提供するインターフェース required_function() で呼び出すのを Adaptee クラスの provided_function_1() メソッドだけにしたことです (今回のシステムで必要な外部機能はこのメソッドが提供するものだけ、という前提です)。もし他の属性 (provided_function_2 など) の呼び出しが行われた場合、__getattr__() 特殊関数を通してすべて Adaptee オブジェクトに委ねるようになっています。
こうすることで、アダプタークラスではシステムに必要な Adaptee 機能についてのインターフェースは提供しますがその他のものについては関知しない、という立場を取ることになります (しかし「無視」をするわけではなくちゃんと「取り次ぎ」はしています)。
この __getattr__() は __init__() などと同様 Python における特殊関数 (special methods) です (magic methods / dunder methods)。Python インタプリタがあるオブジェクトにおける属性を探索し見つけられなかった場合、この __getattr__() メソッドを呼び出しその指示に基づいて探索を続けることになります。
さて、Python は duck typing (ダックタイピング) と呼ばれている型付けスタイルを採用しているプログラミング言語ですから、このコード例をより簡略化することが可能です。
Duck Typing
(ダックタイピング)
ダックタイピングを言い表す有名な言葉は、"If it walks like a duck and quacks like a duck, it must be a duck (もしそれがアヒルのように歩き、アヒルのように鳴くのなら、それはたぶんアヒルに違いない)" というものです。つまり、「アヒル」という「種 (タイプ)」をはっきりと継承していなくても、それと同様の「機能」を示すなら、それらを同じものと見なしましょう、ということですね。
これを Python に当てはめて考えてみると、問題になるのは対象とするオブジェクトが「必要とする」インターフェースを提供しているか否かだけである、ということになります。つまり、その機能を提供しているのであれば、そのクラスが共通するインターフェースクラスを継承しているかいないかは問題とはならず、ある共通のインターフェースクラスを「継承しているものと見なして」利用すればよい、ということです。
ですからこのコード例でいえば、required_function() というインターフェースを定義するための WhatIWant クラスは定義してもしていなくても問題にはならず、あるクラスで required_function() という属性が用意され利用できるようになっているならそのクラスは「WhatIWant というインターフェース定義クラスを継承しているに違いない」となるわけです。結果的に、このコードから WhatIWant クラスを削除し、アダプタークラス (ObjectAdapter) ではどのクラスも継承せず (勿論暗黙的に object クラスは継承していますが...) required_function() メソッドだけを実装していれば事足りる、ということになります:
# 本来外部サービスは以下のようにインポートすることになると思いますが、
# この例では全体像を見やすくするため class を記述しています
# from third_party import WhatIHave


class WhatIHave:
"""
Adaptee (持っているもの)

提供されているものですから、このインターフェースを変更することはできません
"""

def provided_function_1(self): print('Via old interface 1')
def provided_function_2(self): print('Via old interface 2')


class ObjectAdapter:
"""
Adapter (仲介役コード)

Adaptee を継承するのではなくそのオブジェクトを保持します
"""

def __init__(self):
self.what_i_have = WhatIHave()

def required_function(self):
self.what_i_have.provided_function_1()

def __getattr__(self, attr):
"""
このアダプタークラスが所持しない属性へのアクセスがあった場合、
Adaptee で処理するようにします。

これによって、コンポジションでの実装に継承の機能を組み込めます。
"""

return getattr(self.what_i_have, attr)


class ClientObject:
"""
Client (サービスの利用者)
"""

def __init__(self):
self.adapter = ObjectAdapter()

def do_something(self):
self.adapter.required_function() # 'Via old interface 1' を出力します

# ObjectAdapter クラスの属性ではない属性へアクセス
self.adapter.provided_function_2() # 'Via old interface 2' を出力します

if __name__ == '__main__':
client = ClientObject()
client.do_something()
Adaptee クラスのインターフェースを覆い隠すための「望ましい」インターフェースを定義した Target クラスがなくなり、Adapter クラスでそのクラスを継承することもなくなった以外はコードに変更はありません。
これが意味することは何でしょうか? Python のような duck typing を採用している言語では、あるインターフェースを他のインターフェースに適合させるためのクラスを別途定義する必要はない、ということです。つまり、アダプターパターンの趣旨と精神に従うために、動的ではない型タイプ言語で必要となる回避策、すなわちインターフェースクラスの定義等の手段を講じなくてもアダプターパターンを採用できる、ということなんですね。従来通り「文字」として記述したものが全て、ではないんです。テクノロジーは進化し変化し続けています。我々実装者もそうでなければいけません。
NOTE
しかし、ダックタイピング言語のこの特徴は欠点にも直結しています。インターフェースクラスを定義する制約がない、ということは、裏返せばどのクラスがどんなインターフェースを提供しているのかチェックする術がない、ということにも繋がり、乱用/記述ミスなどによるバグを発見しにくくします。そのために Python で導入されたのが「抽象基底クラス (ABCs: Abstract Base Classes)」です。
我々の最新のコード例では、継承ではなくコンポジションを利用して、より Pythonic な方法でこの structural design pattern (構造に関するデザインパターン) を実装しています。そして、アダプターパターンの採用によって、サービスクラスのソースコードに手を触れることなく自分たちが望むインターフェースでサービスにアクセスすることが可能になりました。また、Eiffel プログラミング言語の作成者である Bertrand Meyer によって提唱された open/closed principle (開放/閉鎖の原則) と idea of design by contract (契約プログラミングの考え) に違反せずに済んでいます。
Open/closed principle とは次のようなものです: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification (クラス、モジュール、関数といったソフトウェア構成要素 [エンティティ] は、拡張に対しては開いており、変更に対しては閉じているべきである). つまり、既に存在するソースコードに手を加えることなくエンティティの機能拡張を可能にするための実装原則、です。
何か新しいものを学ぼうとするとき、特にそれが深く広大な範囲にわたるものである場合、そこにある情報を深いレベルまで理解しよう、と務めることが必要です。物事を深く理解するための効果的な方法は、その「大きな」物事を「より小さな」パートに分割し、さらにそれらを理解可能な形に「単純化」することです。そしてこれら「単純化」したものを利用して、問題と解決策を理解するための「直感」を磨き上げていくことです。
自分のその能力に少し自身が持てるようになったら、より複雑な課題に挑戦します。分割したそれぞれのパートについての理解が進んだら、今度はそれらを組み立てて全体を再構築します。この過程を経てもう一度全体を眺めたとき、自分の理解がよりクッキリと明瞭なものになっていることに気付けるはずです。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)