【デザインパターン】アダプター(Adapter)パターンの本質とは?原点に戻って考え直してみる。

この記事は、Design Patterns(いわゆる GoF 本)の一次ソースである原典にあたり、GoF が提唱したデザインパターンの本質を理解しようと試みた記録です。

今回はアダプターパターンについて調べます。

原典は Amazon で購入できます(Kindle 版もあります)。
※画像をクリックすると Kindle 版の購入ページに飛びます(アフィリエイトではありません)。

もくじ

何故原典をあたるのか?

・原典は英語なので、日本語に翻訳された書籍や資料は翻訳ミスがあったり、意図の解釈を間違っている可能性があります。
・仮に母国語が英語だとしても、提唱者の意図を正しく読み取れていない可能性があります。

原典を調べても、上記2点を克服できる保証はありません。
より深く学習するきっかけにはなります。

翻訳方法について

原文を読んで自分で翻訳したあと、以下のツールを使用して確認・修正しています。
最近の翻訳ツールはとても優秀です。

DeepL 翻訳ツール
英辞郎 on the WEB

アダプターパターンの基本情報(翻訳)

クラス、オブジェクト構造:アダプター

意図

クラスの設計をクライアントが要求する他の設計に変換する。
原文:Convert the interface of a class into another interface clients expect.

アダプターは互換性のない設計のせいで他の方法では不可能だったクラス間の共同作業を行わせる。
原文:Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

他に類似するもの

アダプターは、ラッパー(Wrapper)としても知られている。

動機

再利用を前提に設計されたツールキットのクラスが、アプリケーションが必要とするドメイン固有のインターフェイス(the domain-specific interface)と一致しないために、再利用できないことがある。

原典に書いてあるままなのですが、何を言っているのか分からないです。

「ドメイン固有のインターフェイスってなに?」

読み飛ばしてもおおまかな意味は読み取れます。
インターフェイス(設計)に互換性がないせいで再利用できないことがある…と言いたいようです。

ただ、更に疑問が沸きます。

「設計に互換性がない」というのはどういうこと?

これは具体例がないとイメージがつかないですね…。
ちょっとここでは保留しておきます。

アダプターパターンが使える状況(翻訳)

以下の状況でアダプターパターンを使う。

  • 既存クラスを使いたいが、必要なクラスと設計が合わない。
  • 無関係なクラスや未知のクラスなど…必ずしも互換性を持っているとは限らないクラスと連携する再利用可能なクラスを作りたい。
  • (オブジェクトアダプターのみ)複数のサブクラスを使う必要があるが、それらを全てサブクラス化して設計を合わせるのが現実的でない。オブジェクトアダプターであれば、親クラスの設計に合わせることができる。

構造(翻訳)

アダプタークラスは、ある設計から他の設計に合わせるため、複数の継承を行う。

オブジェクトアダプターはオブジェクトの作り(構成)に依存する。
An object adapter relies on object composition:

上記は、原典に掲載されているクラス図。
Wikipedia(日本語/英語)や、参考資料に掲載されているアダプターパターンのクラス図と微妙に違う。
継承と委譲の2つの方法がある…といった記載もないが、クラス図から読み取れる。
オブジェクトアダプターの Adaptee は複数持つことができる。

関係するオブジェクト(Participants)(翻訳)

  • Target(ターゲット)
    Client が利用するドメイン固有の設計を定義する。
  • Client(クライアント)
    Target の設計に合わせたオブジェクトとやり取りする。
  • Adaptee(アダプティー)
    合わせる必要のある既存の設計を定義する。
  • Adapter(アダプター)
    Adaptee の設計を Target の設計に合わせる。

協力関係・関係性(Collaborations)(翻訳)

そういう意味じゃない

Client(クライアントは複数ある場合もある)は、Adapter インスタンスが持つ命令を呼ぶ(実行する)。
次に、Adapter は Request を実行する Adaptee の命令を呼ぶ(実行する)。

原文
Clients call operations on an Adapter instance.
In turn, the adapter calls Adaptee operations that carry out the request.

クラス図だと Client は Target にアクセスする構図になっているが、Target は Adapter の親クラスなので、実際は直接アクセスすることはできない。Client が実際にアクセスするのは Adapter のインスタンスになる。

総論・まとめ(Consequences)(翻訳)

クラスアダプターとオブジェクトアダプターはそれぞれ異なるトレードオフを持つ。

クラスアダプター
  • クラスアダプターは、実際の Adaptee クラスに任せることで Adaptee を Target に合わせる。その結果、あるクラスとそのサブクラス全てを合わせたい場合にクラスアダプターは機能しないと思われる。
  • クラスアダプターは、Adapter が Adaptee のサブクラスであるため、Adaptee の挙動を Adapter にオーバーライドさせる。
  • クラスアダプターは1つのオブジェクトしか導入しないため、Adaptee を取得するのにポインタによる追加の間接参照を必要としない。(?)
ポインタによる追加の間接参照を必要としない…とは?

後述のオブジェクトアダプターは、複数の Adaptee のインスタンスを持つ仕組みなので、インスタンスを介して Adaptee に間接的にアクセスするという意味かと思います。

※C# や Java などのポインタを使わない言語だとしても、インスタンスの中身はポインタです。
※C# はポインタを使えますが、どうしても使う必要があるときだけ使います。通常は参照を使います。

オブジェクトアダプター
  • オブジェクトアダプターは、1つの Adapter が多くの Adaptee、つまり、Adaptee 自身とそのサブクラス(もしあれば)すべてと連携させることができる。
  • オブジェクトアダプターは、Adaptee の動作をオーバーライドしにくくする。オーバーライドするには、Adapteeをサブクラス化し、Adapter が Adaptee 自体ではなく、サブクラスを参照するように実装する必要がある。
アダプターパターンを使う際に問題となる点

長すぎるので割愛。

関連するパターン

ブリッジ(Bridge)…アダプターとよく似ていて区別が難しい。

デコレーター(Decorator)…そのまんま、盛る(デコる)ための仕組み。

ここからが本題

今までの情報は基礎をおさらいするため、原典に忠実に翻訳した内容を掲載したものです。
これから「アダプターパターンって結局何なのよ?」というのを考えていきます。

アダプターパターンで解決したい問題

互換性のない設計で作られているものを、自分が扱っているシステムで動かすこと

ぱっと思いつくケース

プラットフォームごとに作りが違うミドルウェアやAPI
バージョン互換のないデバイスドライバ
Direct Input と XInput の両方の入力をサポートするゲーム
Microsoft Word と Google Document のどちらも扱えるドキュメント作成アプリ
複数の銀行を扱っているATM
支払方法が複数あるオンライン決済

もう一度仕組みを見なおしてみる

書籍『Head First デザインパターン』に掲載されていた図が分かりやすかったので、そちらを引用します。

『Head First デザインパターン』は、具体例が多く掲載されている上、内容も分かりやすいのでお勧めです。
Kindle 版がないので、冊子を購入する必要があります。

アフィリエイトリンクではないので、安心してクリックして下さい。

↑の図のように、コンセントの形が合わないので、間に変換アダプタをかまして、ぷっさせるようにすること。

これをコードでやれば良い…というだけです。
今自分が使っている環境に合わせて細かいところをアレンジする必要はありますが、どんな風にでもやれそうな気がします。

問題を解決できれば手段は問わないのでは?

何でクラス図があるんでしょうか?
手段を限定することで使える状況が狭まってしまうのでは?
アダプターパターンは、どのような型にはめることを想定しているのでしょうか?

何故クラス図のような構造が必要になるのか?

ここからはクラス図を元に考えていく必要があるので、具体的なコード(C#)で見ていきます。

Adapter も Adaptee も使わない場合のコード

class Target {
  public void Request() {}
}

// Client 側
var target = new Target();
target.Request();

Client → Target の部分だけ C# のコードにしたものです。

クラス図を見ると、継承先の Adapter クラスで Request メソッドを呼び出すので、以下のようになるはずです。

class Target {
  public void Request() {}
}

class Adapter : Target {
}

var target = new Adapter();
target.Request();

ただ、このままだと Target.Request がそのまま処理されるだけです。
Target は Adaptee と互換性がないので、間に Adapter を挟み、Adapter が両者を繋ぐ…という話でした。

 Adapter が Target.Request を処理し、Adaptee も処理できるようにするには? 



そのためには、Target.Request を Adapter 側で override する必要があります。

class Target {
  public virtual void Request() {
    /* Adaptee と互換性のない処理をしている */
  }
}

class Adapter : Target {
  public override void Request() {
    base.Request(); // Target.Request を処理する
    // ここで Adaptee の処理も行う
  }
}

var target = new Adapter();
target.Request();

ここで、宙に浮いていた疑問を蒸し返します。

 原典に出てくる互換性…という言葉ですが、互換性があるのか、ないのかを何をもって判断するのでしょう? 



原典だと、インターフェース(設計)という言葉が使われているので、設計に互換性がある・ないということになります。

設計に互換性があるとは?



こういうことでしょうか…?

Target と互換性のある Adaptee

class CompatibleAdaptee {
  public virtual void Request() {}
}

class Target : CompatibleAdaptee {
  public override void Request() {
    base.Request(); // CompatibleAdaptee.Request を呼べるし
    Console.WriteLine("Target.Request"); // オーバーライドもできる
  }
}

Target と互換性がない Adaptee その1

class IncompatibleAdaptee {
  void Request() {}
}

class Target : IncompatibleAdaptee {
  // エラー
  // 親の Request() が private なのでオーバーライドできない
  public override void Request() {}
}

この場合、Adapter をかましたところで、どうにもならないです。
Adaptee の機能を使うことが前提なので、継承先または外部から呼べる必要があるので、このケースは含まれません。

Target と互換性がない Adaptee その2

class IncompatibleAdaptee {
  public void Request() {}
}

class Target : IncompatibleAdaptee {
  // エラー
  // 親の Request() をオーバーライドすることはできないが
  public override void Request() {
  }
  new public void Reuqest() {
    base.Request(); // 呼ぶことはできる
  }
}

あれ? これって互換性あるのでは?

Target と互換性がない? Adaptee その3

class IncompatibleAdaptee {
  public void SpecificRequest() {}
}

class Target : IncompatibleAdaptee {
  public void Request() {
    base.SpecificRequest(); // オーバーライドはできないが呼ぶことはできる
  }
}

ん?

どうも Target と Adaptee が親子関係にあるという前提は成り立たないようです。

クラス図でも Target と Adaptee は親子関係にないですね。

親子関係にある場合、以下のどちらかになってしまいます。

  1. オーバーライドできないし、Adaptee の修正もできないが、Adapter をかます必要がない。
  2. オーバーライドして Adaptee を修正できるので、Adapter をかます必要がない。

「アダプターパターン要らないじゃん」という話になってしまいます。

 そもそも、アダプターパターンでは Target.Request を変更できる・変更して良い…という前提があるのでしょうか? 

これまでの流れから考えると、どうも、そうではないですね。

アダプターパターンの定義に足りない前提条件は?

Target と互換性がない Adaptee その4

class CompatibleAdaptee {
  public void Request() {}
}

class IncompatibleAdaptee {
  public void SpecificRequest() {}
}

class Target {
  CompatibleAdaptee adaptee = new ();
  public void Request() {
     adaptee.Request();
  }
}

var target = new Target();
target.Request(); // CompatibleAdaptee.Request が呼ばれる
target.Request(); // このまま IncompatibleAdaptee.SpecificRequest を呼ぶには?
例えば、
Target.Request で CompatibleAdaptee を使っているが、新しく対応するプラットフォームで CompatibleAdaptee を使うとクラッシュしてしまう。
CompatibleAdaptee は A 社の製品だが、修正を依頼しても時間がかかるとのことで、納期には間に合わない。
B 社の製品 ImcompatibleAdaptee なら機能も同じでクラッシュも起きない。
Target.Request に修正を入れると既に配信している製品に影響が出てしまうので、変更した場合のチェックコストが大きい。
どんなにチェックしたところで、その変更によって必ず不具合を起こす環境が出る。逆もあり得るが、クレームが来るのは前者。
なので、Target.Request を変更せず、IncompatibleAdaptee に切り替える形で対応したい…。

みたいなケースを思いつきました。

Target と CompatibleAdaptee を変更することなく、IncompatibleAdaptee にも対応するには?
これなら、Adapter をかます必要がありそうです。

アダプターパターンが解決したい問題というのは、この状態を示していたということが分かりました。

原典に足りない情報
Target と Adaptee は変更できない(変更してはいけない)。

原典に明示されてはいませんが、改めて考えてみると、そりゃそうだろうという話でした…。
Target を変更できるなら変更すれば良いですし、Adaptee を変更できるなら変更すれば良いだけの話です。
どっちも変えられないから、Adapter が間に入って両者を合わせるという役割が成立します。

Adapter が Target を継承する理由

target.Request(); // このまま Incompatible.SpecificRequest を呼ぶには?

この部分を変更できないという条件なら、やれることは2つです。

  1. Adapter が Target を継承して多態性(ポリモーフィズム)を利用する。
  2. Adapter が Target と同じ名前のメソッドを持つ。
1. Adapter が Target を継承して多態性(ポリモーフィズム)を利用する。

Adaptee はちょっと置いておきます。

class Target {
  public void Request() {}
}

class Adapter : Target {
  new public void Request() {
    // base.Request() は互換性がないので使わない
  }
}

Target target = new Adapter();
target.Request(); // この場合、Target.Request が呼ばれる
class Target {
  public virtual void Request() {}
}

class Adapter : Target {
  public override void Request() {
    // base.Request() は互換性がないので使わない
  }
}

Target target = new Adapter();
target.Request(); // この場合、Adapter.Request が呼ばれる

前者は C# 独自の言語仕様ですが、呼ばれるのは Target の方になります。
アダプターパターンが意図しているのは、Adapter の Request メソッドなので、当てはまるのは後者になります。
(Target の Request メソッドは Adaptee と互換性がないという前提なので。)

ということは、Target の Request メソッドは virtual でないと成立しません。

柔軟な設計を行うために、以下の条件にあてはまる場合、メソッドは virtual にしておくべきです。

  • クラスが継承を禁止していない(C# なら sealed クラス、C++ なら final クラス)
    ※現在は継承禁止でも、継承可能に変更される可能性は常にあるので、この条件は必須ではない。
    ※長期間運用しないコードなら、未来のどこかで変更される可能性を考えなくて良いこともある。
  • interface クラスでない
  • abstract メソッドでない
  • public または protected メソッドである
    ※private でも、未来のどこかで protected や public に変更される可能性はあるので必須ではない。
    ※private メソッドに virtual を付けると、Visual Studio のインテリセンスや静的解析ツールでエラー判定を食らうかも知れないので、そういった事情でできないこともある。

でないと、何らかの問題が起きたときに、アダプターパターンを使って解決する…という選択肢がなくなり、もっと面倒な方法で解決しなければならなくなるかも知れません。

柔軟性を持たせるべきか、柔軟性をなくして堅牢に作るべきか?はトレードオフです。

C# なら以下のように書くこともできます。

abstract class Target {
  public abstract void Request(); // abstract メソッドは実装できない
}

class Adapter : Target {
  public void Request() {} // 子クラス側で処理を実装する
}

Target target = new Adapter();
target.Request(); // この場合、Adapter.Request が呼ばれる

この場合、Target と Adaptee には互換性があるんでしょうか?ないんでしょうか?

Target.Request は abstract メソッドなので、Target 側で処理を実装できません。
つまり、Target.Request は何もしません。

何もしないものと、何かをするものを比べたときに、両者の間で互換性というものが成立するのか?




おそらく、false ですね。
どちらも何かをする場合にのみ、互換性という関係が成立するんじゃないでしょうか?

言い換えると、存在しないものと存在するものとの間で関係を持つことはできるのか?
あるいは、自分ひとりだけが存在している状況で他者との関係を築くことはできるのか?

数式(0 ≠ 1)ならできそうですが、モノでは無理です。
デザインパターンはモノ(オブジェクト)を上手く扱うための仕組みなので、後者だと思います。

であれば、Target.Request が abstract メソッドの場合は、アダプターパターンとは言えません。
Target.Request が virtual の場合のみ、アダプターパターンが成立することになります。

2. Adapter が Target と同じ名前のメソッドを持つ。
class Adapter {
  public void Request() {}
}

Adapter target = new Adapter();
target.Request(); // メソッドの名前は同じだが、Target との関係がなくなる

この場合、Target が無関係になってしまうので、Adapter が Target のように振る舞うことができなくなります。
例えば、以下のような運用をしている場合に対応できません。

class Target {
  public virtual void Request() {}
}
class Target1 : Target {}
class Target2 : Target {}
class Target3 : Target {}

Target targets = new [] {
 new Target1(),
 new Target2(),
 new Target3(),
};
foreach (Target target in targets) {
  target.Request();
}

であれば、Adapter は Target のように振る舞う多態性を持つ必要があります。

このことから、Adapter は Target を継承する必要があると分かります。

結論

Adapter が Target を継承しないと、Adapter が Target であるように振る舞うことができない。
Adapter をあたかも Target であるかのように扱うことで、Target.Request の呼び出しを、代わりに Adapter.Request が行うことになる。
それによって、Target と Adaptee の間に Adapter を かませたことになる。

じっくりと考察してみましたが、結局、アダプターパターンを使おうとすると、クラス図通りになる…ということが分かりました。

え?マジ?それどこ情報?

Target.Request を使えないなら、使わなければいいのでは?

class Target {
  public void Request() {}
}

class IncompatibleAdaptee {
  public void SpecificRequest() {}
}

class Adapter : IncompatibleAdaptee {
  public void Request() {
    base.SpecificRequest();
  }  
}

Target target = new Adapter(); // エラー:Adapter は Target じゃないのでムリ
Adapter adapter = new ();      // これってアダプターなの?

// もはやアダプターをかませる意味がない
IncompatibleAdaptee adaptee = new Adapter();

ムリっぽい…。

Adapter adapter = new (); // これってアダプターなの?

何もかましてないので、Adapter とは呼べないシロモノです。
IncompatibleAdaptee.SpecificRequest を呼び出してるだけです。
継承する必要性も、継承する意味もないですね。

// もはやアダプターをかませる意味がない
IncompatibleAdaptee adaptee = new Adapter();

これも IncompatibleAdaptee を参照してるだけなので、Adapter 要らないですね。

あれ?オブジェクトアダプターは?

やり方は多少違いますが、クラスアダプターと考え方は同じです。
Adaptee を複数扱う必要がある場合、Adaptee はひとつしかないけど Target と多重継承できない場合に代用できます。

参考資料

『Design Patterns: Elements of Reusable Object-Oriented Software (English Edition) 1st 版』

Gang of Four が提唱する23のデザインパターン本の原典です。

『オブジェクト指向における再利用のためのデザインパターン』

原典を日本語翻訳した書籍です。アクセス指定子の private が私的に…と訳されていたり、気になるところはありますが、原典と合わせて読むと良いと思います。

『Game Programming Patterns』

ゲームプログラミングでよく使われるデザインパターンと使い方について書かれています。

『Head First デザインパターン』

デザインパターンについて分かりやすく説明している本です。入門書として最適。

原典のまとめ資料(英語)

https://repository.essex.ac.uk/10654/1/CSM-472.pdf

デザインパターンや定石のまとめサイト(英語)

https://sites.google.com/site/designpatterndetection/gof-specifications/patterns/adapter

Wikipedia

Adapter パターン

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です