Menu
Coddy logo textTech

アダプターパターン

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を使用します。

challenge icon

チャレンジ

簡単

異なるオーディオライブラリのインターフェースを橋渡しするメディアプレイヤーアダプターシステムを構築しましょう。レガシーなオーディオライブラリを最新のメディアプレイヤーアプリケーションに統合することを想像してみてください。古いライブラリは完璧に動作しますが、そのインターフェースはプレイヤーが期待するものと一致しません。これこそが、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つの入力を読み取ります:

    1. 再生するファイル名 (文字列)
    2. 音量レベル (整数、0-100)

    LegacyAudioLib インスタンスと、それをラップする AudioAdapter を作成します。次に、MediaPlayer インターフェースを介してアダプターを使用し(MediaPlayer ポインタとして保存します)、以下の操作を行います:

    1. 音量を入力されたレベルに設定する
    2. 入力されたファイル名を再生する
    3. 再生を停止する

例えば、入力が song.mp375 の場合:

Legacy: Volume set to 0.75
Legacy: Loading song.mp3
Legacy: Playback started
Legacy: Playback stopped

入力が podcast.wav50 の場合:

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;
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン