6. アダプターパターン (Adapter)
互換性のないインターフェースを合わせて繋げてくれるアダプターパターンについて学びましょう。
ひとことで言うと
形の合わないオブジェクトをうちの約束に合う殻で包んで、元のコードを直さなくても一緒に使えるようにするパターンです。
問題: 約束の違うよそのクラス
デリバリーのお店の支払いプログラムを作るとしましょう。うちのお店には、支払い方法が守る約束があります。
- 支払い方法なら
〈支払う〉ができなければならず、金額は円で受け取ります。
カード払いはこの約束をきちんと守っています。ところが、海外のお客さんのために、ほかの会社が作ったグローバルペイをつなぐことになりました。グローバルペイはよそのクラス(パッケージ)なので直せず、形がまったく違います。
- メソッドの名前が
〈支払う〉ではなく〈送金〉です。 - 引数の順番が違い、通貨を先に、金額を後に受け取ります。
- 金額の単位が円ではなくドルです。ここでは簡単に、100円を1ドルとして扱います。
まず思いつくのは、支払う場所ごとに「グローバルペイならこう呼ぶ」を確かめる方法です。
(参考:うちのお店の支払いの約束です)
【支払い】を規定しよう:
〈支払う〉がなければならない(【数字】の『円』)
【支払い】に従う【カード払い】を設計しよう:
〈支払う〉を作ろう(【数字】の『円』):
枠「[カード] {『円』}円を支払い」を出力しよう
(参考:ほかの会社が作った支払いの道具なので直せません。名前も、順番も、単位も違います)
【グローバルペイ】を設計しよう:
〈送金〉を作ろう(【文字列】の『通貨』, 【数字】の『金額』):
枠「[グローバルペイ] {『金額』} {『通貨』}を送金」を出力しよう
(参考:支払う場所ごとに「グローバルペイなら…」を確かめます)
〈注文の支払い〉を作ろう(『支払い方法』, 【数字】の『円』):
もし(『支払い方法』が【グローバルペイ】の一種だ)なら:
『支払い方法』の〈送金〉(「USD」, 『円』 / 100)を実行しよう
それ以外なら:
『支払い方法』の〈支払う〉(『円』)を実行しよう
〈配達料の支払い〉を作ろう(『支払い方法』, 【数字】の『円』):
もし(『支払い方法』が【グローバルペイ】の一種だ)なら:
『支払い方法』の〈送金〉(「USD」, 『円』 / 100)を実行しよう
それ以外なら:
『支払い方法』の〈支払う〉(『円』)を実行しよう
〈注文の支払い〉(新しい【カード払い】(), 1500)を実行しよう
〈注文の支払い〉(新しい【グローバルペイ】(), 1500)を実行しよう
〈配達料の支払い〉(新しい【グローバルペイ】(), 300)を実行しよう
実行結果
[カード] 1500円を支払い
[グローバルペイ] 15 USDを送金
[グローバルペイ] 3 USDを送金
ちゃんと動きます。でも、支払う場所が増えると痛くなります。
- 支払う場所がグローバルペイの事情を全部知っていなければなりません。
〈注文の支払い〉は「お金を受け取る」ことだけを知っていればいいのに、グローバルペイのメソッド名、引数の順番、ドルの計算まで知っています。 - 同じ変換がコピーされます。
〈注文の支払い〉と〈配達料の支払い〉に、まったく同じもしが入っています。返金や定期払いができたらまたコピーする必要があり、為替レートが変われば全部の場所を探して直さなければなりません。 - 約束で守ってあげられません。
『支払い方法』に【支払い】の型を付けるとグローバルペイを渡せないので、どんな型でも受け取れるように開けておくしかありません。見当違いのものが入ってきても、カナデは止められません。 - 支払い方法が1つ増えると、
もしも1つ増えます。「かんたん決済B」がまた別の形なら、支払う場所ごとに道が1つずつ増えます。
解決策: 合わないオブジェクトを合う殻で包む
アダプターパターンは、よそのクラスを直さずに、うちの約束に従う殻を1つ作って包みます。
- うちの約束(
【支払い】)はそのままにします。 - アダプターを作ります。アダプターは
【支払い】に従い、中にグローバルペイを1つ包んでいます。 - アダプターの
〈支払う〉は円を受け取り、名前、引数の順番、単位を変えてからグローバルペイの〈送金〉を呼びます。変換はここ1か所だけにあります。 - 支払う場所は、もう
【支払い】だけを知っています。カードでもグローバルペイでも、同じように〈支払う〉を呼びます。
たとえ: 旅行用の変換プラグ
海外旅行でノートパソコンの充電器を挿そうとしたら、コンセントの形が違いました。充電器を分解して直したり、ホテルの壁を変えたりはしませんよね。代わりに変換プラグを1つはさみます。片側は充電器に合い、もう片側は壁に合います。充電器はいつもどおり挿すだけです。この変換プラグが「アダプター」です。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| うちの約束 | 【支払い】(規定) |
うちのコードが期待する形です |
| 合わないオブジェクト | 【グローバルペイ】 |
役には立つけれど、形が違います。直せません |
| アダプター | 【グローバルペイアダプター】 |
うちの約束に従い、中でグローバルペイの形に変えて呼びます |
| 使う場所 | 〈注文の支払い〉、〈配達料の支払い〉 |
【支払い】だけを知っていて、だれでも同じように呼びます |
カナデで作る
同じ支払いプログラムをアダプターパターンで作り直してみましょう。【グローバルペイ】は1文字も直していません。
(参考:うちのお店の支払いの約束です)
【支払い】を規定しよう:
〈支払う〉がなければならない(【数字】の『円』)
【支払い】に従う【カード払い】を設計しよう:
〈支払う〉を作ろう(【数字】の『円』):
枠「[カード] {『円』}円を支払い」を出力しよう
(参考:ほかの会社が作った支払いの道具なので直せません。名前も、順番も、単位も違います)
【グローバルペイ】を設計しよう:
〈送金〉を作ろう(【文字列】の『通貨』, 【数字】の『金額』):
枠「[グローバルペイ] {『金額』} {『通貨』}を送金」を出力しよう
(参考:グローバルペイを包んで、うちの約束に合わせるアダプターです)
【支払い】に従う【グローバルペイアダプター】を設計しよう:
『グローバル』を【グローバルペイ】の空っぽに隠そう
最初に作られる時(【グローバルペイ】の『グローバル』)次のようにしよう:
『私』の『グローバル』を『グローバル』にしよう
〈支払う〉を作ろう(【数字】の『円』):
(参考:名前、引数の順番、単位を、ここで1回だけ変えます)
『私』の『グローバル』の〈送金〉(「USD」, 『円』 / 100)を実行しよう
(参考:支払う場所は、もう【支払い】だけを知っています)
〈注文の支払い〉を作ろう(【支払い】の『支払い方法』, 【数字】の『円』):
『支払い方法』の〈支払う〉(『円』)を実行しよう
〈配達料の支払い〉を作ろう(【支払い】の『支払い方法』, 【数字】の『円』):
『支払い方法』の〈支払う〉(『円』)を実行しよう
『海外のお客さんの支払い』を【支払い】の新しい【グローバルペイアダプター】(新しい【グローバルペイ】())にしよう
〈注文の支払い〉(新しい【カード払い】(), 1500)を実行しよう
〈注文の支払い〉(『海外のお客さんの支払い』, 1500)を実行しよう
〈配達料の支払い〉(『海外のお客さんの支払い』, 300)を実行しよう
実行結果
[カード] 1500円を支払い
[グローバルペイ] 15 USDを送金
[グローバルペイ] 3 USDを送金
結果はリファクタリング前と同じです。でも〈注文の支払い〉と〈配達料の支払い〉は、もう1行ずつです。
1つずつもう一度
- うちの約束はそのまま。
【支払い】も【カード払い】も変わっていません。アダプターパターンは、すでにちゃんと動いている側には触りません。 - よそのクラスもそのまま。
【グローバルペイ】も1文字も変わっていません。パッケージを新しいバージョンに上げても、うちが直した部分とぶつかることはありません。 - 変換はアダプター1か所だけ。 メソッド名(
〈支払う〉→〈送金〉)、引数の順番(通貨が先)、単位(円 → ドル)を、【グローバルペイアダプター】の〈支払う〉1か所で変えます。為替レートが変わったら、ここだけ直せば済みます。 - 約束で守れるようになった。
〈注文の支払い〉が【支払い】の『支払い方法』だけを受け取るように変えられました。アダプターを通さないグローバルペイをそのまま渡すと、カナデがその場で止めます。
アダプターとデコレーターは、どちらも「包む」ものでは?
そのとおり、どちらもオブジェクトを包みます。ただし、包む目的が違います。アダプターは形(約束)を変えるために包みます。グローバルペイは〈送金〉ができますが、包むと〈支払う〉をするオブジェクトになります。デコレーターは形はそのままで機能を足すために包みます。コーヒーを包んでも、コーヒーのままです。
新しい支払い方法を足してみると
形がまた違う「かんたん決済B」をつなぐとしましょう。
- 前のやり方: 支払うすべての場所に
もしくは(『支払い方法』が【かんたん決済B】の一種だ)なら:を入れなければなりません。 - アダプターパターン:
【支払い】に従う【かんたん決済Bアダプター】を1つだけ作れば済みます。支払う場所はそのままです。
やってみよう
上のコードに【ポイントモール】というよそのクラスを足してみてください。〈ポイント差し引き〉(【数字】の『ポイント』)というメソッドがあり、1ポイントは10円です。それから【ポイントモールアダプター】を作って、〈注文の支払い〉(新しい【ポイントモールアダプター】(新しい【ポイントモール】()), 1500)が150ポイントを差し引くようにしてみましょう。〈注文の支払い〉は直さなくていいはずです。
どんな時に使うといいの?
- 使いたいクラスがあるのに、形(メソッドの名前、引数、単位)がうちの約束と違う時
- そのクラスを直せない、または直したくない時(よそのパッケージ、古いコード、ほかのチームのコード)
- 形の違ういくつものオブジェクトを、1つの約束で同じように扱いたい時
- よそのクラスが変わっても、うちのコードは1か所だけ直したい時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| うちのコードも、よそのコードも直しません | クラス(アダプター)が1つ増えます |
| 変換がアダプター1か所にまとまっています | 1回多く経由して呼ぶので、読んでいく時に1段階増えます |
| 使う場所は、うちの約束1つだけを知っていれば済みます | 2つの形が違いすぎる(必要な情報がそもそもない)と、アダプターでも合わせにくいです |
| 約束(型)で、間違ったものを止められます |
早すぎるパターンは毒です。
よそのクラスを1か所で1回だけ呼ぶなら、その場で合わせて呼んでも十分です。同じ変換を2回目にコピーしようとした瞬間が、アダプターを作る時です。そして、直せるうちのコードどうしの形が合わないだけなら、アダプターより片方の名前をすぐ直すほうがすっきりすることが多いです。
ほかのパターンとの関係
- デコレーターパターンも包む形です。 アダプターは形を変えるために、デコレーターは形はそのままで機能を足すために包みます。
- ファサードパターンと比べてみましょう。 アダプターはオブジェクト1つの形を変え、ファサードはいくつものオブジェクトをまとめて、もっと簡単な窓口を1つ新しく作ります。
- ストラテジーパターンと一緒に使えます。 よそが作った計算の道具を戦略として使いたいのに形が違うなら、アダプターで包んで戦略の約束に合わせればいいんです。