JavaJava [ジャバ]オブジェクト指向プログラミング言語でシステム開発をしていると、「既存の便利なクラスを使いたいけれど、今求めているインターフェース(メソッド名や引数の型など)と合わない…」という問題に直面することがあります。
このようなインターフェースのズレを吸収し、既存のコードを変更せずに再利用できるようにするのが、GoF(Gang of Four)のデザインパターンの1つである Adapter(アダプター)パターン です。
本記事では、Adapterパターンの概要や現実世界での例え、2種類の実装方法(クラスとオブジェクト)、そして具体的なJavaコード例まで分かりやすく解説します。
1. JavaのAdapterパターン(アダプターパターン)とは?
Adapterパターンは、その名の通り「アダプター(変換器)」の役割を果たすデザインパターンです。 互換性のないインターフェースを持つクラス同士を繋ぎ、連携して動作できるようにする ことを目的としています。
既存のクラス(提供されている側)のソースコードに手を加えることなく、クライアント(利用する側)が期待するインターフェースに変換(適合)させることができるため、サードパーティ製のライブラリやレガシーコードを活用する際に非常に役立ちます。
現実世界での例え:ACアダプターをイメージしよう
身近な例として、スマートフォンの充電などに使う「ACアダプター」を想像してみてください。
- Target(目的):機器が利用できる「直流(DC)」を求めている
- Adaptee(既存のもの):家庭用のコンセントは「交流(AC)」を提供している
- Adapter(適合者):ACアダプターが「交流(AC)」を「直流(DC)」に変換する
このように、仕様が異なる2つの間に入って橋渡しをするのがAdapterの役割です。
2. Adapterパターンの4つの構成要素(Target・Adapteeなど)
Adapterパターンには、主に以下の4つの役割(構成要素)が登場します。
- Target(対象) クライアントが使いたい(期待している)インターフェースです。
- Adaptee(適合される側) 既に存在しているが、Targetとはインターフェースが合わないクラスです(レガシーコードなど)。
- Adapter(適合者) Targetのインターフェースを実装(または継承)し、内部でAdapteeのメソッドを呼び出すことで、ズレを吸収するクラスです。
- Client(利用者) Targetのインターフェースを使って処理を行うクラスです。
3. Adapterパターンの2種類の実装方法とJavaコード例
JavaにおけるAdapterパターンの実装には、大きく分けて以下の2つの手法があります。
- 継承を使った実装(クラスアダプター)
- 委譲(コンポジション)を使った実装(オブジェクトアダプター)
それぞれの違いを具体的なコードとともに見ていきましょう。
ここでは、新しいシステムで使いたい ModernPrinter(Target)と、既存の古い印刷クラス LegacyPrinter(Adaptee)があると仮定します。
① 継承を使った実装方法(クラスアダプター・Class Adapter)
クラスアダプターは、既存のクラス(Adaptee)を継承し、同時にTargetインターフェースを実装することで変換を行います。
public interface ModernPrinter { void modernPrint(String text);}public class LegacyPrinter { public void legacyPrint(String text) { System.out.println("【旧型プリンター】: " + text); }}// Targetインターフェースを実装し、Adapteeクラスを継承するpublic class PrinterClassAdapter extends LegacyPrinter implements ModernPrinter { @Override public void modernPrint(String text) { // 継承しているLegacyPrinterのメソッドを呼び出す this.legacyPrint(text); }}public class Main { public static void main(String[] args) { // クライアントはModernPrinterとして扱うことができる ModernPrinter printer = new PrinterClassAdapter(); printer.modernPrint("こんにちは、クラスアダプター!"); }}【特徴】 Javaではクラスの多重継承ができないため、Class Adapterを実装する場合、一般的にはAdapteeを継承しつつ、Targetをインターフェースとして実装します。
② 委譲を使った実装方法(オブジェクトアダプター・Object Adapter)
オブジェクトアダプターは、既存のクラス(Adaptee)のインスタンスを内部に保持し、その処理を委譲することで変換を行います。オブジェクト指向設計における「継承よりもコンポジションを優先する」という原則(合成優先の原則)とも相性がよく、一般的に実務ではこちらの手法が推奨されることが多いです。
// Targetインターフェースのみを実装するpublic class PrinterObjectAdapter implements ModernPrinter {
// Adapteeのインスタンスを保持する private LegacyPrinter legacyPrinter;
// コンストラクタでAdapteeを受け取る public PrinterObjectAdapter(LegacyPrinter legacyPrinter) { this.legacyPrinter = legacyPrinter; }
@Override public void modernPrint(String text) { // 保持しているインスタンスに処理を委譲する legacyPrinter.legacyPrint(text); }}public class Main { public static void main(String[] args) { // 既存のインスタンスを生成 LegacyPrinter legacyPrinter = new LegacyPrinter();
// アダプターに既存のインスタンスを渡す ModernPrinter printer = new PrinterObjectAdapter(legacyPrinter); printer.modernPrint("こんにちは、オブジェクトアダプター!"); }}【特徴】 Adapteeの継承を必要としないため柔軟性が高く、Targetがクラスの場合でも、Adapter自身がTargetを継承することで対応できます(※今回のコード例ではTargetをインターフェースとして実装しています)。また、Adapteeのサブクラスに対しても同じアダプターを使い回すことができます。
4. Adapterパターンを導入するメリット・デメリット
Adapterパターンのメリット(既存コードの再利用)
-
開放閉鎖原則(OCP)と相性がよい
既存のコードを変更せずに新しいAdapterを追加して対応できるため、開放閉鎖原則(OCP)の考え方と相性がよいです。テスト済みの既存クラス(Adaptee)を一切書き換えることなく、新しいインターフェースに対応させることができます。
-
コードの再利用性が高まる
古いシステムやサードパーティ製のライブラリを、現代のシステム要件に合わせて簡単に統合できます。
Adapterパターンのデメリット(構成の複雑化)
-
クラスの数が増える
アダプタークラスを新しく作成する必要があるため、全体のコード量やクラス数が増加し、構成が複雑になる可能性があります。
-
根本的な解決ではない場合がある
もし既存クラス(Adaptee)自体を変更できる環境であり、かつ影響範囲が小さいのであれば、直接リファクタリングしてしまった方がコードがシンプルになるケースもあります。
5. まとめ:JavaのAdapterパターンで柔軟な設計を実現しよう
JavaのAdapterパターンは、「インターフェースが合わない既存のクラスを、新しい要件に合わせて変換する」 非常に実用的なデザインパターンです。
- クラスアダプター(継承): Adapteeを継承して実装。Javaでは多重継承の制約に注意。
- オブジェクトアダプター(委譲): Adapteeのインスタンスを保持して実装。より柔軟であり、合成優先の設計原則からも実務で推奨されることが多い。
「この便利なライブラリを使いたいけど、メソッド名が違うな…」という状況に遭遇したら、既存コードを無理にいじる前に、ぜひAdapterパターンの導入を検討してみてください。
以上で本記事の解説を終わります。
よいITライフを!
人気記事
- 1
- 2
- 3
- 4
- 5
思考の整理というテーマを、実務ですぐに使える具体的な「技術」として落とし込んだ一冊です。