広告

Java : Adapter パターン (図解/デザインパターン)

Adapter パターンとは、GoF によって定義されたデザインパターンの1つです。
既存のクラスの修正なしに、インタフェースを変更して再利用できます。

本記事では、Adapter パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

図解/デザインパターン一覧

概要

Adapter パターン(アダプター・パターン)とは、GoF によって定義されたデザインパターンの1つである。Adapter パターンを用いると、既存のクラスに対して修正を加えることなく、インタフェースを変更することができる。

Adapter パターンとは、

  • 既存のクラスの 修正なし に、インタフェースを変更して再利用する

という設計です。

Adapter は、日本語的に発音すると「アダプター」となります。
意味は「適合させるもの」ですね。

日本語でもそのまま「アダプター」で使われているので馴染みやすいかと思います。

【Adapter パターンのイメージ】

イメージ図1

対象となるオブジェクトを、アダプターでラッピングするイメージですね。


身近にある例でも考えてみましょう。
電力を供給するコンセントの形状についてです。

日本のコンセントは、縦長の2本のスリットになっています。
しかし、海外では別の形状をしていることがあります。

イメージ図2

当然、日本の電化製品を海外に持っていっても、海外のコンセントにプラグは挿し込めません。
コンセントとプラグの形状が違うからですね。

イメージ図3

海外旅行に行ったことのあるかたは、コンセントの変換プラグを準備されたのではないでしょうか。
例えば次のような商品です。

エレコム 海外用 電源変換プラグ 4種セット(SE/C/O/BF) 二重安全設計 T-HPSETWH ホワイト

この変換プラグは、

  • 既存のインタフェース (日本の形状) を変更しないで、
  • 別のインタフェース (海外の形状) に対応する

という機器です。
このおかげで、海外用の電化製品を、わざわざ旅行のために買いなおさなくてもよいわけですね。

イメージ図4

Adapter パターンに当てはめると、このコンセント変換プラグが アダプター に相当します。
なんとなくイメージはできましたでしょうか…?

構造は Proxy パターン によく似ています。
しかし、Proxy パターンは 同じインタフェース を提供するのに対して、Adapter パターンは 異なるインタフェース に変換します。

このような Adapter パターンですが、注意することもあります。

  • 異なるインタフェース に変換するため、そこに無理が生じる
  • 小さなクラスに Adapter パターンを適用した結果、無駄に複雑になってしまった

なんてこともあるかもしれません。

Adapter パターンは万能ではありません。
よく検討してから使いたいですね。

関連記事:


クラス図とシーケンス図

Adapter パターンは、その実装の手法により

  • 委譲を利用するパターン
  • 継承を利用するパターン

の2つに分類されます。

おすすめは委譲を利用するパターンです。

継承は 強い依存 になります。
それに比べると、委譲は 弱い依存 です。

依存はなるべく弱くしたほうが、クラスの汎用性や柔軟性が高くなるためです。

委譲を利用するパターン

クラス図1

シーケンス図1

上の図は、委譲を利用した Adapter パターンの一般的なクラス図とシーケンス図です。

Adaptee が 修正したくない クラスです。
Adapter と名前が似ているのでご注意ください。

Adaptee の methodB() を、新しいインタフェースである Tareget の methodA() に変換したいわけですね。

それでは、このクラス図をそのままコードにしてみましょう。

まずは Adaptee クラスです。

public class Adaptee {
    public void methodB() {
        System.out.println("Method B!");
    }
}

そして、新しいインタフェースとなる Target です。

public interface Target {
    void methodA();
}

最後に Adapter クラスです。
委譲を利用するため、フィールドに Adaptee オブジェクトを持ちます。

public class Adapter implements Target {
    private final Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void methodA() {
        adaptee.methodB();
    }
}

それでは、これらのクラスを使ってみましょう。

final var target = new Adapter(new Adaptee());
target.methodA();

// 結果
// ↓
//Method B!

結果も問題なしですね。
methodA() -> methodB() へと変換されました。

継承を利用するパターン

クラス図2

シーケンス図2

上の図は、継承を利用した Adapter パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。

Adaptee と Target は 委譲を利用するパターン と同じです。

public class Adaptee {
    public void methodB() {
        System.out.println("Method B!");
    }
}

public interface Target {
    void methodA();
}

Adapter クラスでは、Target を実装(implements) して、かつ、Adaptee を継承(extends) します。

public class Adapter extends Adaptee implements Target {
    @Override
    public void methodA() {
        methodB();
    }
}

それでは、これらのクラスを使ってみましょう。

final var target = new Adapter();
target.methodA();

// 結果
// ↓
//Method B!

結果も問題なしですね。


具体的な例

もう少し具体的な例も見てみましょう。
足し算と掛け算をする 計算機クラス(Calc) を考えてみます。

Calc は Calculator(計算機) を短く表記したものです。

クラス図3

LibCalc は外部ライブラリのクラスの1つです。
赤の他人が作ったライブラリなので、簡単には修正できません。

public class LibCalc {
    private int result;

    public void start(int value) {
        result = value;
    }

    public void add(int value) {
        result += value;
    }

    public void multiply(int value) {
        result *= value;
    }

    public void printResult() {
        System.out.println("result = " + result);
    }
}

各メソッドの説明

  • start : 計算開始
  • add : 足し算
  • multiply : 掛け算
  • printResult : 結果を表示

実際の使い方も見てみましょう。

  • 2+3=5
  • 3×4=12

この2つを計算してみます。

final var calc = new LibCalc();

// ------------
// 2+3=5
calc.start(2);
calc.add(3);

calc.printResult();

// 結果
// ↓
//result = 5

// ------------
// 3×4=12
calc.start(3);
calc.multiply(4);

calc.printResult();

// 結果
// ↓
//result = 12

さて、LibCalc は使い方に少しくせがあります。
自分の設計しているコード内では使いづらいので、代わりに MyCalc インタフェースを用意しました。

public interface MyCalc {

    void add(int value1, int value2);

    void multiply(int value1, int value2);

    void printResult();
}
  • add : 足し算 (value1 + value2)
  • multiply : 掛け算 (value1 * value2)
  • printResult : 結果を表示

それでは、Adapter パターンを使って LibCalc を再利用してみましょう。

public class CalcAdapter implements MyCalc {
    private final LibCalc libCalc;

    public CalcAdapter(LibCalc libCalc) {
        this.libCalc = libCalc;
    }

    @Override
    public void add(int value1, int value2) {
        libCalc.start(value1);
        libCalc.add(value2);
    }

    @Override
    public void multiply(int value1, int value2) {
        libCalc.start(value1);
        libCalc.multiply(value2);
    }

    @Override
    public void printResult() {
        libCalc.printResult();
    }
}

これらのクラスを使う例です。

final var calc = new CalcAdapter(new LibCalc());

// ------------
// 2+3=5
calc.add(2, 3);
calc.printResult();

// 結果
// ↓
//result = 5

// ------------
// 3×4=12
calc.multiply(3, 4);
calc.printResult();

// 結果
// ↓
//result = 12

無事、LibCalc を使ったときと同じ結果になりました。
問題なしですね。

注意

  • 分かりやすさを優先するために、今回の例では とても小さなクラス に対して無理に Adapter パターンを使いました。
    このくらいの規模のクラスであれば、Adapter パターンを使わずにそのまま実装してしまったほうがシンプルになるでしょう。

まとめ

Adapter パターンとは、

  • 既存のクラスの 修正なし に、インタフェースを変更して再利用する

という設計です。

ただし、

  • 異なるインタフェース に変換するため、そこに無理が生じる
  • 小さなクラスに Adapter パターンを適用した結果、無駄に複雑になってしまった

なんてことがないように注意したいですね。

よく検討してから使いましょう。


関連記事

ページの先頭へ