概要
既存システムの改修やライブラリ統合の場面では、変更できないクラスのインターフェースが新しい設計と合わないことがよくあります。全面的に書き直す余裕はないが、新しいインターフェースで統一したい――そういったときに Adapter パターンが使えます。Adapter は既存クラス(Adaptee)を内部に持ち、Target インターフェースのメソッド呼び出しを Adaptee のメソッドに変換します。Java 標準ライブラリでも InputStreamReader(InputStream を Reader に変換)や Arrays.asList(配列を List に変換)が同じ構造です。この記事では、レガシーデータ読取クラスを新しいインターフェースに適合させる実装を示し、Adapter を適用すべき場面とブリッジパターンとの違いを整理します。
使いどころ
レガシーシステムの CSV 出力クラスを新しい DataExporter インターフェースに適合させ、他の出力形式と統一的に扱う
外部ライブラリのログ出力をプロジェクト標準のロガーインターフェースに変換し、ログ出力先を一元管理する
古い独自フォーマットの設定ファイル読取クラスを Properties ライクなインターフェースで扱えるようにする
コード例
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);
}
}Version Coverage
record で変換前後のデータを保持でき、var で型推論が使える。Adapter の可読性が向上する。
// 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
注意点
Adapter を多用するとラッパーが何重にも重なり、デバッグ時にどの実体が呼ばれているか追いにくくなる。必要最小限に留める
クラスアダプタ(継承)とオブジェクトアダプタ(委譲)の2方式がある。Java では多重継承ができないため、オブジェクトアダプタ(委譲)が一般的
Adapter は既存クラスの「インターフェースの不一致」を解消するパターン。機能を追加したい場合は Decorator を検討する
FAQ
Adapter はインターフェースを変換するのが目的で、機能は変えません。Decorator は同じインターフェースを保ちつつ、機能を追加します。
Java では多重継承ができないため、委譲を使うオブジェクトアダプタが一般的です。Adaptee のメソッドをオーバーライドする必要がある場合のみクラスアダプタを検討します。
InputStreamReader(InputStream を Reader に変換)、OutputStreamWriter、Arrays.asList(配列を List に変換)が代表例です。