概要

既存システムの改修やライブラリ統合の場面では、変更できないクラスのインターフェースが新しい設計と合わないことがよくあります。全面的に書き直す余裕はないが、新しいインターフェースで統一したい――そういったときに Adapter パターンが使えます。Adapter は既存クラス(Adaptee)を内部に持ち、Target インターフェースのメソッド呼び出しを Adaptee のメソッドに変換します。Java 標準ライブラリでも InputStreamReader(InputStream を Reader に変換)や Arrays.asList(配列を List に変換)が同じ構造です。この記事では、レガシーデータ読取クラスを新しいインターフェースに適合させる実装を示し、Adapter を適用すべき場面とブリッジパターンとの違いを整理します。

使いどころ

レガシーシステムの CSV 出力クラスを新しい DataExporter インターフェースに適合させ、他の出力形式と統一的に扱う

外部ライブラリのログ出力をプロジェクト標準のロガーインターフェースに変換し、ログ出力先を一元管理する

古い独自フォーマットの設定ファイル読取クラスを Properties ライクなインターフェースで扱えるようにする

コード例

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

public class AdapterPatternSample {

    // Target: 新しいインターフェース
    interface Target {
        String readData();
    }

    // Adaptee: 変更できない既存クラス
    static class LegacyDataReader {
        public String fetchRawData() {
            return "ID:001,NAME:田中太郎,AGE:30";
        }
    }

    // 変換結果を保持する record(Java 17+)
    record AdapterResult(String source, String adapted) {}

    // Adapter: Target を実装し、LegacyDataReader に委譲
    static class DataReaderAdapter implements Target {
        private final LegacyDataReader legacy;

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

        @Override
        public String readData() {
            var raw = legacy.fetchRawData();
            return convertFormat(raw);
        }

        public AdapterResult readDataWithResult() {
            var raw = legacy.fetchRawData();
            return new AdapterResult(raw, convertFormat(raw));
        }

        private String convertFormat(String raw) {
            var pairs = raw.split(",");
            var sb = new StringBuilder("[");
            for (int i = 0; i < pairs.length; i++) {
                sb.append(pairs[i].replace(":", "="));
                if (i < pairs.length - 1) sb.append(", ");
            }
            return sb.append("]").toString();
        }
    }

    public static void main(String[] args) throws IOException {
        // 自作 Adapter
        var adapter = new DataReaderAdapter(new LegacyDataReader());
        System.out.println("変換後: " + adapter.readData());

        var result = adapter.readDataWithResult();
        System.out.println("変換前: " + result.source());
        System.out.println("変換後: " + result.adapted());

        // 標準ライブラリの Adapter 例
        var bytes = "Hello".getBytes("UTF-8");
        var reader = new InputStreamReader(
            new ByteArrayInputStream(bytes), "UTF-8");
        System.out.println("InputStreamReader: " + (char) reader.read());

        var list = Arrays.asList("Java 8", "Java 17", "Java 21");
        System.out.println("Arrays.asList: " + list);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で変換前後のデータを保持でき、var で型推論が使える。Adapter の可読性が向上する。

Java 17
// Java 17: var + record で変換結果を保持
var adapter = new DataReaderAdapter(new LegacyDataReader());
record AdapterResult(String source, String adapted) {}
var result = adapter.readDataWithResult();
System.out.println("変換前: " + result.source());

Library Comparison

標準 API(interface + 委譲)既存クラスを薄くラップするだけで済む場合。依存なしで書ける。変換ロジックが複雑になると Adapter クラスが肥大化する。
Spring(HandlerAdapter)Spring MVC で異なるハンドラー型を統一的に扱うとき。Spring の Adapter パターン適用例として参考になる。Spring フレームワーク内の話であり、汎用のアダプタとは用途が異なる。

注意点

Adapter を多用するとラッパーが何重にも重なり、デバッグ時にどの実体が呼ばれているか追いにくくなる。必要最小限に留める

クラスアダプタ(継承)とオブジェクトアダプタ(委譲)の2方式がある。Java では多重継承ができないため、オブジェクトアダプタ(委譲)が一般的

Adapter は既存クラスの「インターフェースの不一致」を解消するパターン。機能を追加したい場合は Decorator を検討する

FAQ

Adapter と Decorator の違いは何ですか。

Adapter はインターフェースを変換するのが目的で、機能は変えません。Decorator は同じインターフェースを保ちつつ、機能を追加します。

クラスアダプタとオブジェクトアダプタはどちらを使うべきですか。

Java では多重継承ができないため、委譲を使うオブジェクトアダプタが一般的です。Adaptee のメソッドをオーバーライドする必要がある場合のみクラスアダプタを検討します。

Java 標準ライブラリの Adapter にはどんなものがありますか。

InputStreamReader(InputStream を Reader に変換)、OutputStreamWriter、Arrays.asList(配列を List に変換)が代表例です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます