Adapter パターン — 外部ライブラリをドメインに染めない防波堤
既存の API を自分たちのドメインに合わせて包むアダプタ。クラスアダプタ vs オブジェクトアダプタ、SLF4J や Spring の HandlerAdapter などの実例まで。

どういう問題を解くパターンか
既存の 「使いにくい API」 を、呼び出し側が期待する 「きれいなインタフェース」 に馴染ませる パターン。
例:
- 外部ベンダーの SDK がドメインに不要な
Map<String, Object>を返してくる → 自分のドメインオブジェクトに変換して返すアダプタを挟む - 古いバージョンの API と新しいバージョンの API を切り替えたい → 共通インタフェースを定義して両方をアダプト
- サードパーティのログライブラリをプロダクトコードに直接露出させたくない → SLF4J のような 「ログインタフェース」 で包む
本質は「依存の境界を 1 層ずらす」 こと。 外部 API の仕様変更がドメイン層にまで波及しないよう、 Adapter 1 ヶ所だけが影響を吸収します。
いつ使う / 使わない
| 使うべき | 使わないほうがよい |
|---|---|
| 外部 SDK の API がドメインと合わない | SDK がそのままきれいに使える |
| 複数実装を 1 つのインタフェースで扱いたい | 実装が 1 つしかない |
| テスト時に差し替えたい (fake を注入) | 差し替えの必要がない |
2 種類の実装: Object Adapter と Class Adapter
Object Adapter (推奨)
委譲 (has-a) で包む形。実務ではほぼこれ 。
// Target: 自分たちが使いたいインタフェース
public interface FileConfig {
void load(String filename) throws IOException;
void save(String filename) throws IOException;
String get(String key);
void set(String key, String value);
}
// Adaptee: 既存の API (java.util.Properties を再利用したいとする)
import java.util.Properties;
// Adapter: Properties を FileConfig として見せる
public final class PropertiesFileConfig implements FileConfig {
private final Properties properties = new Properties();
@Override
public void load(String filename) throws IOException {
try (var in = new FileInputStream(filename)) {
properties.load(in);
}
}
@Override
public void save(String filename) throws IOException {
try (var out = new FileOutputStream(filename)) {
properties.store(out, "");
}
}
@Override
public String get(String key) {
return properties.getProperty(key, "");
}
@Override
public void set(String key, String value) {
properties.setProperty(key, value);
}
}
Class Adapter (継承ベース、推奨しない)
継承 (is-a) で既存クラスを拡張する形。 Java は単一継承なので使える場面が狭く、 Adaptee のメソッドが外から見えてしまう (カプセル化が崩れる) 弱点があります。
public class PropertiesFileConfig extends Properties implements FileConfig {
// 継承によって Properties の全メソッドも露出してしまう
...
}
基本は Object Adapter。Class Adapter は Adaptee を 「ちょっとだけ拡張したい」 ような 限定的なケースにのみ使います。
使用例
public class Main {
public static void main(String[] args) throws IOException {
FileConfig config = new PropertiesFileConfig();
config.load("app.properties");
config.set("vendor.apple", "iOS");
config.set("vendor.google", "Android");
config.save("app.properties");
}
}
呼び出し側は FileConfig という自分たちのインタフェースにしか依存していません。
将来 YAML に乗り換えるときは YamlFileConfig implements FileConfig を追加するだけで済みます。
実例 (JDK / Spring / OSS)
Adapter は「依存の境界を切る」ための基本道具で、至る所にあります。
java.util.Arrays.asList(T...)— 配列をリストインタフェースにアダプトjava.io.InputStreamReader— バイトストリームを文字ストリームにアダプト- SLF4J — Logback / Log4j / JUL を統一インタフェースにアダプト- Spring MVC
HandlerAdapter— Controller の多様な書き方 (@Controller/ 関数型 / HttpRequestHandler) を統一実行- Spring DataJpaRepository,MongoRepository,R2dbcRepository— 保存先を抽象化
落とし穴
1. アダプタに業務ロジックを書き始める
Adapter はあくまで 「変換・委譲」 に専念させます。 変換時にビジネスルールを混ぜ始めると、将来の実装差し替えで事故ります。
// NG: アダプタの中にドメイン判定が混じる
@Override
public String get(String key) {
String v = properties.getProperty(key);
if (v == null && "api.key".equals(key)) {
return System.getenv("DEFAULT_KEY"); // ← ドメイン判定
}
return v;
}
ドメイン判定は呼び出し側 (ドメイン層) に戻しましょう。
2. Adaptee の例外をそのまま漏らす
外部 SDK 固有の例外 (VendorSdkException) をアダプタの外に投げてしまうと、
呼び出し側が外部ライブラリに依存します。
Adapter は ドメイン例外に変換する境界 でもあります。
try {
externalSdk.call();
} catch (VendorSdkException e) {
throw new ConfigLoadException("failed to load", e);
}
3. アダプタのスコープが広すぎる
1 つのアダプタで何でも吸収しようとすると、肥大化して神クラス化します。 「読み込み」 「書き込み」 「変換」 で分けるなど、 責務単位で小さく保つ のが Adapter の読みやすさの鍵。
まとめ: Adapter は 「依存を外に出すための防波堤」 です。 外部ライブラリ・レガシー API・バージョン切り替えが絡む場面では、 最初に Adapter を 1 枚挟んでおくことで、将来の変更コストが大きく下がります。
関連記事
- アルゴリズム·
Java デザインパターン総論 — 現場で本当に効くパターンの選び方
GoF の 23 パターンから、現代の Java 開発で実際に効くものを選別する指針と、Spring/JDK で見つかる実例マップ。
- アルゴリズム·
AbstractFactory パターン — 「関連するオブジェクト群」を一式で切り替える
Factory Method との違いを 1 分で整理し、OS 別 UI / DB 別 Repository / テナント別実装など、実務での使いどころを示す。
- アルゴリズム·
FactoryMethod パターン — Spring BeanFactory の背骨を自作する
インスタンス生成の種類を抽象側に閉じ込める Factory Method パターン。JDK/Spring の実例、Simple Factory との違い、現代 Java での書き方まで。