java-recipes

ホーム デザインパターン › DP-06

DP-06: Adapter パターン

既存クラスを変更せず、新しいインターフェースに適合させるパターンです。java.io.InputStreamReader が その代表例で、InputStream(バイト列)を Reader(文字列)として扱えるようにします。

Adapter パターンとは

Adapter パターンは「あるクラスのインターフェース(メソッドのシグネチャ)を、 クライアントが期待する別のインターフェースに変換する」パターンです。 電源プラグの変換アダプターのように、互換性のないものを接続する役割を担います。 特に外部ライブラリや既存の古いコード(レガシーコード)をそのまま使いながら、 新しいシステムのインターフェースに合わせるときに役立ちます。

クラスアダプター vs オブジェクトアダプター

項目クラスアダプター(継承)オブジェクトアダプター(委譲)
仕組みAdaptee クラスを継承するAdaptee のインスタンスを保持する
Java での実現extends で実装フィールドとして保持して委譲
推奨度Java は多重継承不可のため制限ありJava では一般的にこちらを推奨

Java 標準ライブラリの代表例としてjava.io.InputStreamReaderがあります。これは InputStream(バイト列を扱う)をReader(文字列を扱う)インターフェースに適合させる Adapter です。 また Arrays.asList() も配列をList インターフェースに適合させる Adapter と見なせます。

サンプルコード

AdapterPatternSample.java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Arrays;
import java.util.List;

public class AdapterPatternSample {

    // 新しいシステムが期待するインターフェース(Target)
    interface Target {
        String readData();
    }

    // 古いシステムのクラス(Adaptee): 変更できない想定
    static class LegacyDataReader {
        public String fetchRawData() {
            return "ID:001,NAME:田中太郎,AGE:30";
        }
    }

    // Adapter クラス: Target を implements し、LegacyDataReader に委譲する
    // これは「オブジェクトアダプター(委譲型)」と呼ばれる実装方式です
    static class DataReaderAdapter implements Target {

        // 既存クラスのインスタンスを保持(委譲)
        private final LegacyDataReader legacyReader;

        public DataReaderAdapter(LegacyDataReader legacyReader) {
            this.legacyReader = legacyReader;
        }

        @Override
        public String readData() {
            // 既存クラスのメソッドを呼び出し、形式を変換する
            String rawData = legacyReader.fetchRawData();
            return convertFormat(rawData);
        }

        // "KEY:VALUE,KEY:VALUE" 形式を "[KEY=VALUE, KEY=VALUE]" 形式に変換
        private String convertFormat(String rawData) {
            String[] pairs = rawData.split(",");
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < pairs.length; i++) {
                sb.append(pairs[i].replace(":", "="));
                if (i < pairs.length - 1) {
                    sb.append(", ");
                }
            }
            sb.append("]");
            return sb.toString();
        }
    }

    // InputStreamReader: Java 標準ライブラリの Adapter 例
    // InputStream(バイト列)を Reader(文字列)インターフェースに適合させます
    static void showInputStreamReaderExample() throws IOException {
        String text = "こんにちは、Java!";
        byte[] bytes = text.getBytes("UTF-8");

        // InputStream を Reader に変換する Adapter(Java 標準ライブラリ)
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        Reader reader = new InputStreamReader(inputStream, "UTF-8");

        StringBuilder sb = new StringBuilder();
        int ch;
        while ((ch = reader.read()) != -1) {
            sb.append((char) ch);
        }
        System.out.println("InputStreamReader の出力: " + sb.toString());
    }

    // Arrays.asList(): 配列(Array)を List インターフェースに適合させる Adapter
    static void showArraysAsListExample() {
        String[] array = {"Java 8", "Java 17", "Java 21"};
        List<String> list = Arrays.asList(array);
        System.out.println("Arrays.asList の出力: " + list);
    }

    public static void main(String[] args) throws IOException {
        System.out.println("=== Adapter パターン ===");
        System.out.println();

        LegacyDataReader legacyReader = new LegacyDataReader();

        System.out.println("[旧インターフェース(直接呼び出し)]");
        System.out.println("fetchRawData(): " + legacyReader.fetchRawData());

        System.out.println();

        // Adapter を通じて Target インターフェースとして扱う
        Target adapter = new DataReaderAdapter(legacyReader);
        System.out.println("[新インターフェース(Adapter 経由)]");
        System.out.println("readData(): " + adapter.readData());

        System.out.println();
        System.out.println("=== Java 標準ライブラリの Adapter 例 ===");

        System.out.println();
        System.out.println("[InputStreamReader: InputStream → Reader]");
        showInputStreamReaderExample();

        System.out.println();
        System.out.println("[Arrays.asList: 配列 → List]");
        showArraysAsListExample();
    }
}

Java 8 では委譲(コンポジション)を使ったオブジェクトアダプターを実装します。既存クラス(LegacyDataReader)を一切変更せず、Adapter クラスが新旧のインターフェースをつなぎます。

よくあるミス・注意点

⚠️ 既存クラスを直接修正してしまう(開放閉鎖原則違反)

Adapter パターンの目的は「既存クラスを変更せずに新インターフェースへ対応させる」ことです。 外部ライブラリや変更してはいけないコードに手を加えてしまうと、 将来のバージョンアップ時に修正が上書きされたり、予期しない不具合を生む原因になります。 Adapter クラスを新しく作ることで、既存クラスは修正なしのまま使い続けられます。

⚠️ Adapter に業務ロジックを追加しすぎる

Adapter の役割は「インターフェースの変換」だけです。 データの加工や複雑な業務ロジックを Adapter に詰め込むと、 本来の役割が曖昧になり、テストや保守が難しくなります。 変換以外の処理は別のクラスに分けましょう。

⚠️ Arrays.asList() の戻り値に要素を追加しようとする

Arrays.asList() が返すリストは固定サイズで、add()remove() を呼ぶとUnsupportedOperationException がスローされます。 要素を追加したい場合は new ArrayList<>(Arrays.asList(...)) でコピーしましょう。

テストする観点

  • Adapter の readData() が正しい形式([KEY=VALUE, ...])に変換されること
  • 元の LegacyDataReader クラスが変更されていないこと(fetchRawData() が元の形式を返すこと)
  • 入力データにカンマが含まれない場合(フィールドが1つ)でも正しく動作すること(境界値)
  • 入力データが空文字の場合に空の結果が返ること(境界値)
  • Adapter を Target 型として扱ったとき、ポリモーフィズムが正しく機能すること
  • Java 17 版: AdapterResult record の source()adapted() がそれぞれ正しい値を返すこと

GitHub でソースコードを見る →