Adapter
一言でいうと
Adapter (アダプター、 適合装置) は、 構造に関するデザインパターンの一つで、 非互換なインターフェースのオブジェクト同士の協働を可能とします。
![Adapter デザインパターン](/images/patterns/content/adapter/adapter-ja.png?id=1d4a040176811ac41a6ac9da77e49708)
問題
株式市場監視アプリを作成することを想像してみてください。 このアプリは、 複数の情報源から XML 形式の株式データをダウンロードし、 見栄えの良いグラフや図表をユーザーに表示します。
ある時点で、 外部提供の気の利いた分析ライブラリーを統合してアプリを改善することに決めました。 ただし、 一つ問題があります。 分析ライブラリーは、 JSON 形式のデータでのみ機能するということです。
![分析ライブラリーと統合前のアプリの構造](/images/patterns/diagrams/adapter/problem-ja.png?id=6cea2062034f0e193956ccdbd52c7686)
分析ライブラリーは、 自分のアプリとは非互換な形式のデータしか受け付けないので、 そのままでは使用できない。
XML で動作するようにライブラリーを変更することもできます。 ただそうすると、 ライブラリーに依存した既存のコードが動かなくなる可能性があります。 さらに、 そもそもライブラリーのソースコードが入手できない可能性があり、 その場合この手法はうまくいきません。
解決策
アダプター を作成します。 これは、 あるオブジェクトのインターフェースを他のオブジェクトが理解できるように変換する特殊なオブジェクトです。
アダプターは、 オブジェクトをラップして、 舞台裏で行われる変換の詳細を隠蔽します。 ラップされたオブジェクトは、 アダプター内で動作しているということすら認識していません。 たとえば、 メートルとキロメートルで動作するオブジェクトを、 すべてのデータをフィートやマイルに変換するアダプターでラップすることができます。
アダプターは、 データの形式変換だけだけではなく、 異なるインターフェースのオブジェクト同士の協働を可能とします。 仕組みは次のとおりです。
- アダプターは、 既存オブジェクトの一つと互換なインターフェースを実装します。
- このインターフェースを使用して、 既存オブジェクトはアダプターのメソッドを安全に呼び出すことができます。
- 呼び出されると、 アダプターはリクエストを二つ目のオブジェクトに渡します。 ただし、 二つ目のオブジェクトが期待する形式で渡します。
時には、 呼び出しを双方向に変換できる双方向アダプターを作成することも可能です。
![Adapter の解決策](/images/patterns/diagrams/adapter/solution-ja.png?id=1022dd0f6e8530da0f952e391c5fae75)
株式市場アプリの話に戻りましょう。 形式非互換の問題を解決するために、 コードが直接接触する分析ライブラリーのすべてのクラスに対して XML から JSON へのアダプターを作成します。 次に、 これらのアダプターを介してのみライブラリーーと通信するようにコードを調整します。 アダプターは呼び出しを受けると、 着信 XML データを JSON に変換し、 ラップされた分析オブジェクトの適切なメソッドを呼び出します。
現実世界でのたとえ
![Adapter パターンの例](/images/patterns/content/adapter/adapter-comic-1-ja.png?id=3a71208ff4ee01f4e4029cc84482f859)
海外旅行前と後のスーツケース
米国からヨーロッパに初めて旅行する時、 ラップトップを充電しようとすると驚くかもしれません。 電源プラグとソケットの規格は国によって異なるのです。 そのため、 米国のプラグはドイツのソケットに入りません。 この問題は、 アメリカ式ソケットとヨーロッパ式プラグを備えた電源プラグ・アダプターを使用することで解決できます。
構造
オブジェクト・アダプター
この実装では、 オブジェクトの合成原理を使用します。 アダプターは一方のオブジェクトのインタフェースを実装し、 もう一方のオブジェクトをラップします。 これは、 一般的に普及しているすべてのプログラミング言語で実装可能です。
![Adapter デザインパターンの構造(オブジェクト・アダプター)](/images/patterns/diagrams/adapter/structure-object-adapter.png?id=33dffbe3aece294162440c7ddd3d5d4f)
![Adapter デザインパターンの構造(オブジェクト・アダプター)](/images/patterns/diagrams/adapter/structure-object-adapter-indexed.png?id=a20b311948b361a058097e5bcdbf067a)
-
クライアント (Client) は、 既存のビジネス・ロジックを含んだクラスです。
-
クライアント・インターフェース (Client Interface) は、 クライアント側コードと協働するためには従わなければいけないプロトコル=決まり事を記述しています。
-
サービス (Service) は、 外部から提供されるか、 昔から使われてきた、 ある役に立つクラスです。 このクラスは、 非互換のインターフェースのため、 クライアントから直接使うことができません。
-
アダプター (Adapter) は、 クライアントとサービスの両方と機能できるコードです: クライアント・インターフェースを実装すると同時にサービス・オブジェクトをラップします。 アダプターは、 アダプターのインターフェースを介してクライアントから呼び出され、 それをラップされたサービス・オブジェクトが理解できる形式に変換して呼び出します。
-
クライアント側コードは、 クライアント・インタフェースを介してアダプターとやりとりする限り、 具象アダプター・クラスと結合されることはありません。 このおかげで、 新しい種類のアダプターをプログラムに導入しても既存のクライアント側コードは問題なく動作します。 これは、 サービス・クラスのインターフェースを変更したり置き換えたりする際に便利です。 クライアント側コードを変更することなく、 新しいアダプター・クラスを作成することができます。
クラス・アダプター
この実装では継承を利用します。 アダプターは、 両方のオブジェクトのインターフェースを同時に継承します。 この方法は、 C++ のような多重継承をサポートするプログラミング言語でのみ実装可能であることにご注意ください。
![Adapter デザインパターンの構造(クラス・アダプター)](/images/patterns/diagrams/adapter/structure-class-adapter.png?id=e1c60240508146ed3b98ac562cc8e510)
![Adapter デザインパターンの構造(クラス・アダプター)](/images/patterns/diagrams/adapter/structure-class-adapter-indexed.png?id=250b5c485a7dfba7c16b89a9201538fb)
-
クラス・アダプター では、 オブジェクトをラップする必要はありません。 クライアントとサービスの両方から振る舞いを継承するからです。 適合 (変換) は、 上書きされたメソッドの間で行われます。 完成したアダプターは、 既存のクライアント側クラスの代わりに使うことができます。
擬似コード
この Adapter パターンの使用例は、 矛盾の典型である 「丸い穴に四角いくい」 (訳注: 合わないものを無理に一緒にすることを表現する英語の慣用句) に基づいています。
![Adapter パターン例の構造](/images/patterns/diagrams/adapter/example.png?id=9d2b6857ce256f2c669383ce4df3d0aa)
四角いくいを丸い穴に適応中
アダプターは、 半径が四角の対角線の半分の長さ (言い換えると、 四角いくいを含むことのできる最小の円の半径) である丸いくいのふりをします。
// 互換なインターフェースを持つ二つのクラス RoundHole と RoundPeg を考
// 察。
class RoundHole is
constructor RoundHole(radius) { …… }
method getRadius() is
// 穴の半径を返す。
method fits(peg: RoundPeg) is
return this.getRadius() >= peg.getRadius()
class RoundPeg is
constructor RoundPeg(radius) { …… }
method getRadius() is
// くいの半径を返す。
// しかし、SquarePeg は、非互換なクラス。
class SquarePeg is
constructor SquarePeg(width) { …… }
method getWidth() is
// 四角いくいの幅を返す。
// アダプター・クラスを使えば、四角いくいを丸い穴に合わせられる。RoundPeg
// クラスを拡張してアダプター・オブジェクトが丸いくいとして機能。
class SquarePegAdapter extends RoundPeg is
// 実際には、アダプターは SquarePeg クラスのインスタンスを保持。
private field peg: SquarePeg
constructor SquarePegAdapter(peg: SquarePeg) is
this.peg = peg
method getRadius() is
// アダプターは、それが内包する四角いくいが入る半径の丸い杭のふり
// をする。
return peg.getWidth() * Math.sqrt(2) / 2
// クライアント・コードのどこかの様子。
hole = new RoundHole(5)
rpeg = new RoundPeg(5)
hole.fits(rpeg) // true
small_sqpeg = new SquarePeg(5)
large_sqpeg = new SquarePeg(10)
hole.fits(small_sqpeg) // これはコンパイル不可(非互換な型)
small_sqpeg_adapter = new SquarePegAdapter(small_sqpeg)
large_sqpeg_adapter = new SquarePegAdapter(large_sqpeg)
hole.fits(small_sqpeg_adapter) // true
hole.fits(large_sqpeg_adapter) // false
適応性
Adapter クラスは、 既存のクラスを使用したいが、 そのインターフェースが自分のコードの他の部分と互換性がない場合に使用します。
Adapter パターンでは、 自分のクラスと、 昔から引き継がれてきたクラスや外部提供のクラスや奇妙なインターフェースのクラスとの間で翻訳者として機能する中間層のクラスを作成します。
既存のいくつかのサブクラスを再利用したいが、 スーパークラスに追加できる共通機能が欠けている場合に、 このパターンを使用します。
実装方法
-
最低二つの非互換なクラスがあることを確認してください:
- 有益だが変更不能なサービスクラス。 (多くの場合、 外部提供だから、 過去から引き継がれたコードだから、 あるは、 依存する要素の数が多すぎるためといった理由で変更できない。)
- サービス・クラスを利用したい 1 個以上の クライアント・クラス。
-
クライアント・インタフェースを宣言し、 クライアントとサービスとの情報伝達の方法を記述する。
-
クライアント・インターフェースに従うアダプター・クラスを作成。 メソッドはとりあえず空のままにする。
-
アダプター・クラスに、 サービス・オブジェクトへの参照を格納するためのフィールドを追加します。 このフィールドは、 コンストラクタで初期化するのが一般的な方法ですが、 アダプターのメソッドの呼び出し時に渡す方が便利な場合もあります。
-
クライアント・インタフェースの全メソッドをアダプター・クラスで一つずつ実装していってください。 アダプターは、 実際の仕事のほとんどはサービス・オブジェクトに委ね、 インターフェースやデータ形式の変換だけを行うようにします。
-
クライアントは、 クライアント・インタフェースを介してアダプターを使用する必要があります。 そうすることで、 クライアントのコードに影響を与えることなくアダプターの変更や拡張が可能となります。
長所と短所
- 単一責任の原則。 インターフェースやデータ変換のコードを、 プログラムの主要なビジネス・ロジックから分離可能。
- 開放閉鎖の原則。 クライアント・インターフェースを介してアダプターと連携する限り、 既存のクライアント側コードを壊すことなく、 新しい種類のアダプターをプログラムに追加可能。
- 一連の新規のインターフェースとクラスを追加する必要があるため、 全体的なコードの複雑性が増加。 コードの他の部分と一致するようにサービス・クラスを書き直す方が単純でいい場合あり。
他のパターンとの関係
-
Bridge は通常、 アプリケーションの部分部分を独立して開発できるように、 設計当初から使われます。 一方、 Adapter は、 既存のアプリケーションに対して利用され、 本来は互換性のないクラスとうまく動作させるために使われます。
-
Adapter は既存のオブジェクトのインターフェースを変更するのに対し、 Decorator はインターフェースの変更なしにオブジェクトを強力にします。 さらに、 Decorator は、 再帰的な合成をサポートします。 これは、 Adapter を使用する時には不可能です。
-
Adapter はラップされたオブジェクトに対しては異なるインターフェースを提供し、 Proxy は同じインターフェースを提供し、 Decorator は強化したインターフェースを提供します。
-
Facade が既存のオブジェクトに対して新しいインターフェースを定義するのに対し、 Adapter は既存のインターフェースを使えるようにしようとするものです。 Adapter は通常一つのオブジェクトだけを包み込みますが、 Facade は複数のオブジェクトからなるサブシステム全体を相手にします。
-
Bridge、 State、 Strategy (と限られた意味合いでは、 Adapter も) は、 非常に似た構造をしています。 実際のところ、 これらの全てのパターンは、 合成に基づいており、 仕事を他のオブジェクトに委任します。 しかしながら、 違う問題を解決します。 パターンは、 単にコードを特定の方法で構造化するためのレシピではありません。 パターンが解決する問題に関して、 開発者同士がするコミュニケーションの道具でもあります。