アダプターパターン
CoddyのC++ジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 97/104。
アダプターパターン(Adapter pattern)は、互換性のないインターフェースを連携させることができます。これは2つのクラス間の架け橋として機能し、あるクラスのインターフェースをクライアントが期待するインターフェースに変換します。これは、システムのインターフェースと一致しないレガシーコードやサードパーティライブラリを統合する場合に特に便利です。
必要な機能を持つ既存のクラスがあると想像してください。しかし、そのインターフェースはコードが期待するものと一致しません。どちら側も修正する代わりに、既存のクラスをラップして呼び出しを変換するアダプターを作成します:
#include <iostream>
#include <string>
// 互換性のないインターフェースを持つ既存のクラス
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// クライアントが期待するターゲットインターフェース
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// アダプターは LegacyPrinter をラップし、Printer インターフェースを実装します
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // 呼び出しを変換します
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// クライアントは期待されるインターフェースを使用します
adapter.print("Hello World");
}アダプターは LegacyPrinter への参照を保持し、 Printer インターフェースを実装します。 print() が呼び出されると、 printMessage() に処理を委譲します。クライアントコードは、背後にあるレガシーな実装を意識することなく、 Printer インターフェースを使用して動作します。
既存のクラスを使用したいが、そのインターフェースが必要なものと一致しない場合、または互換性のないインターフェースを持つクラスと連携する再利用可能なクラスを作成する必要がある場合に、Adapterを使用します。
チャレンジ
簡単異なるオーディオライブラリのインターフェースを橋渡しするメディアプレイヤーアダプターシステムを構築しましょう。レガシーなオーディオライブラリを最新のメディアプレイヤーアプリケーションに統合することを想像してみてください。古いライブラリは完璧に動作しますが、そのインターフェースはプレイヤーが期待するものと一致しません。これこそが、Adapterパターンの本領が発揮される場面です。
コードは以下の4つのファイルに分けて構成します:
MediaPlayer.h: 最新のメディアプレイヤーが期待するターゲットインターフェースを定義します。以下の純粋仮想メソッドを持つ抽象クラス
MediaPlayerを作成してください:play(const std::string& filename)— 指定されたファイルを再生しますstop()— 再生を停止しますsetVolume(int level)— 音量を設定します (0-100)
仮想デストラクタを含めてください。
LegacyAudioLib.h: 互換性のないインターフェースを持つ既存の「レガシー」オーディオライブラリを作成します。LegacyAudioLibクラスは、プレイヤーが期待するものとは異なる動作をする古いライブラリを表します。以下のメソッドを持つ必要があります:loadAudio(const std::string& path)—Legacy: Loading [path]と出力しますstartPlayback()—Legacy: Playback startedと出力しますstopPlayback()—Legacy: Playback stoppedと出力しますadjustVolume(double percentage)— 小数 (0.0 から 1.0) を受け取り、Legacy: Volume set to [percentage]と出力します(小数値を表示してください)
AudioAdapter.h: 2つのインターフェースを橋渡しするアダプターを構築します。AudioAdapterクラスはMediaPlayerを継承し、LegacyAudioLibオブジェクトへの参照を保持する必要があります。アダプターは、最新のインターフェースからの呼び出しをレガシーなものへと変換します:play()は、レガシーライブラリのloadAudio()とstartPlayback()の両方を呼び出す必要がありますstop()はstopPlayback()に処理を委譲する必要がありますsetVolume()は整数 (0-100) を受け取りますが、レガシーライブラリは小数 (0.0-1.0) を期待しているため、変換が必要です
main.cpp: アダプターの動作を実演します。2つの入力を読み取ります:
- 再生するファイル名 (文字列)
- 音量レベル (整数、0-100)
LegacyAudioLibインスタンスと、それをラップするAudioAdapterを作成します。次に、MediaPlayerインターフェースを介してアダプターを使用し(MediaPlayerポインタとして保存します)、以下の操作を行います:- 音量を入力されたレベルに設定する
- 入力されたファイル名を再生する
- 再生を停止する
例えば、入力が song.mp3 と 75 の場合:
Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped入力が podcast.wav と 50 の場合:
Legacy: Volume set to 0.5
Legacy: Loading podcast.wav
Legacy: Playback started
Legacy: Playback stoppedクライアントコードが完全に MediaPlayer インターフェースのみで動作していることに注目してください。背後でレガシーライブラリが実際の処理を行っていることを、クライアントは関知しません。アダプターは、整数の音量を小数に変換することを含め、すべての変換を処理します。これが Adapter パターンの本質です。つまり、どちらの側も変更することなく、互換性のないインターフェースを連携させるのです。
チートシート
Adapterパターンは、2つのクラス間の架け橋として機能することで、互換性のないインターフェースを連携させることができます。あるクラスのインターフェースを、クライアントが期待するインターフェースに変換します。
既存のクラスを使用したいが、そのインターフェースが必要なものと一致しない場合や、レガシーコードやサードパーティのライブラリを統合する場合に、Adapterパターンを使用します。
構造
このパターンは、主に3つのコンポーネントで構成されています:
- Target Interface:クライアントが期待するインターフェース
- Adaptee:互換性のないインターフェースを持つ既存のクラス
- Adapter:Adapteeをラップし、Target Interfaceを実装して、それらの間の呼び出しを変換します
例
#include <iostream>
#include <string>
// 互換性のないインターフェースを持つ既存のクラス (Adaptee)
class LegacyPrinter {
public:
void printMessage(const std::string& msg) {
std::cout << "Legacy: " << msg << "\n";
}
};
// クライアントが期待するターゲットインターフェース
class Printer {
public:
virtual void print(const std::string& text) = 0;
virtual ~Printer() = default;
};
// AdapterはLegacyPrinterをラップし、Printerインターフェースを実装します
class PrinterAdapter : public Printer {
LegacyPrinter& legacy;
public:
PrinterAdapter(LegacyPrinter& lp) : legacy(lp) {}
void print(const std::string& text) override {
legacy.printMessage(text); // 呼び出しを変換します
}
};
int main() {
LegacyPrinter oldPrinter;
PrinterAdapter adapter(oldPrinter);
// クライアントは期待されるインターフェースを使用します
adapter.print("Hello World");
}アダプターはAdapteeへの参照を保持し、Target Interfaceを実装します。アダプターのメソッドが呼び出されると、必要に応じて呼び出しを変換しながらAdapteeのメソッドに委譲します。クライアントコードは、レガシーな実装を知ることなく、Target Interfaceを介して動作します。
自分で試してみよう
#include <iostream>
#include <string>
#include "MediaPlayer.h"
#include "LegacyAudioLib.h"
#include "AudioAdapter.h"
using namespace std;
int main() {
// 入力を読み込む
string filename;
int volumeLevel;
cin >> filename;
cin >> volumeLevel;
// TODO: LegacyAudioLib のインスタンスを作成する
// TODO: レガシーライブラリをラップする AudioAdapter を作成する
// TODO: アダプターを MediaPlayer ポインタとして保存する
// TODO: MediaPlayer インターフェースを使用して以下を行う:
// 1. 音量を入力されたレベルに設定する
// 2. 入力された filename を再生する
// 3. 再生を停止する
return 0;
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
3コンストラクタとデストラクタ
デフォルトコンストラクタ引数付きコンストラクタコピーコンストラクタムーブコンストラクタコンストラクタ初期化リスト委譲コンストラクタデストラクタ詳解Rule of Three / Five / Zeroまとめ - Stringクラス6演算子オーバーロード
演算子オーバーロード入門算術演算子のオーバーロード比較演算子のオーバーロードストリーム演算子代入演算子のオーバーロード[] と () 演算子のオーバーロード型変換演算子まとめ:Matrix クラス