Engineering
Glossvation Engineering
アルゴリズム

Adapter パターン — 外部ライブラリをドメインに染めない防波堤

既存の API を自分たちのドメインに合わせて包むアダプタ。クラスアダプタ vs オブジェクトアダプタ、SLF4J や Spring の HandlerAdapter などの実例まで。

Glossvation ·

Adapter パターン概念図

どういう問題を解くパターンか

既存の 「使いにくい 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 Data JpaRepository, 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 枚挟んでおくことで、将来の変更コストが大きく下がります。

関連記事