Adapterパターンとは?
一言で言うと、
使い方が自分の望む形と違う場合に、使い方を変換して“使えるようにする”ための仕組みです。
詳しく言うと、
Adapterパターンは、互換性のない2つのインターフェースを橋渡しするデザインパターンです。既存のクラス(Adaptee)を変更せずに、新しいインターフェース(Target)に適合させるためのクラス(Adapter)を作成します。これにより、既存のコードを再利用しつつ、新しいシステムに統合することができます。
日常での具体例
海外旅行で使う電源アダプターを想像してください。日本の電化製品のプラグは海外のコンセントに直接差し込めませんが、電源アダプターを使うことで、異なる規格のコンセントに接続できるようになります。この電源アダプターがAdapterパターンの役割を果たしています。
Adapterパターンのメリット
- 既存のコードを変更せずに再利用できる
- 新旧のシステムを統合しやすくなる
Adapterパターンのデメリット
- 新たなクラス(Adapter)を追加する必要があるため、コードが若干複雑になる
Adapterパターンをコードで説明
今回、想定するケース
古い形式のデータ(CSV形式)を扱うレガシーシステムと、新しい形式のデータ(JSON形式)を扱う新システムを統合する必要があるケースを考えます。
クラス図
サンプルコード
interface Book {
title: string;
author: string;
}
// Target
interface ModernLibrary {
getBooks(): Book[];
}
// Adaptee
class LegacyLibrary {
getLegacyBooks(): string {
return "title,author\nBook1,Author1\nBook2,Author2";
}
}
// Adapter
class LibraryAdapter implements ModernLibrary {
private legacyLibrary: LegacyLibrary;
constructor(legacyLibrary: LegacyLibrary) {
this.legacyLibrary = legacyLibrary;
}
getBooks(): Book[] {
const csvData = this.legacyLibrary.getLegacyBooks();
const lines = csvData.split("\n");
return lines.slice(1).map(line => {
const [title, author] = line.split(",");
return { title, author };
});
}
}
// Client
class BookManager {
useBooks(library: ModernLibrary) {
const books = library.getBooks();
books.forEach(book => {
console.log(`${book.title} by ${book.author}`);
});
}
}
// 使用例
const legacyLib = new LegacyLibrary();
const adapter = new LibraryAdapter(legacyLib);
const manager = new BookManager();
manager.useBooks(adapter);
コードの解説
このコードは Adapterパターン を用いて、互換性のないインターフェイスを橋渡ししている例です。以下に各ポイントを解説します。
1. インターフェイスの定義
interface Book {
title: string;
author: string;
}
interface ModernLibrary {
getBooks(): Book[];
}
-
Bookインターフェイスは、本のタイトルと著者を表します。 -
ModernLibraryインターフェイスは、getBooks()メソッドを持ち、Book[](本の配列)を返します。 - これがターゲット(目標)インターフェイスで、クライアントが利用する形式です。
2. Adaptee(既存の非互換なクラス)
class LegacyLibrary {
getLegacyBooks(): string {
return "title,author\nBook1,Author1\nBook2,Author2";
}
}
-
LegacyLibraryは旧式のクラスで、CSV形式の文字列を返すgetLegacyBooks()メソッドを持っています。 - クライアントの期待する形式(
Book[])と異なるため、そのままでは利用できません。
3. Adapter(アダプター)
class LibraryAdapter implements ModernLibrary {
private legacyLibrary: LegacyLibrary;
constructor(legacyLibrary: LegacyLibrary) {
this.legacyLibrary = legacyLibrary;
}
getBooks(): Book[] {
const csvData = this.legacyLibrary.getLegacyBooks();
const lines = csvData.split("\n");
return lines.slice(1).map(line => {
const [title, author] = line.split(",");
return { title, author };
});
}
}
-
LibraryAdapterはModernLibraryインターフェイスを実装し、LegacyLibraryを内部で利用してそのデータを変換します。 - 具体的には、
getLegacyBooks()のCSVデータをパースして、Book[]の形式に変換しています。
変換の流れ
-
getLegacyBooks()が返す文字列(例:"title,author\nBook1,Author1\nBook2,Author2")を取得。 - 行ごとに分割し、1行目(ヘッダー行)をスキップ。
- 残りの行をパースして、
Bookオブジェクトに変換。 -
Book[]配列を返す。
4. Client(クライアント)
class BookManager {
useBooks(library: ModernLibrary) {
const books = library.getBooks();
books.forEach(book => {
console.log(`${book.title} by ${book.author}`);
});
}
}
-
BookManagerはModernLibraryインターフェイスに依存しています。 -
getBooks()メソッドを使って本のリストを取得し、各本の情報をコンソールに出力します。 - このクライアントコードは
LegacyLibraryを直接利用せず、ModernLibraryインターフェイスを通じてアクセスします。
5. 使用例(Adapterの適用)
const legacyLib = new LegacyLibrary();
const adapter = new LibraryAdapter(legacyLib);
const manager = new BookManager();
manager.useBooks(adapter);
-
LegacyLibraryを直接BookManagerに渡すことはできません。なぜなら、LegacyLibraryはクライアントが期待するModernLibraryインターフェイスを実装していないからです。 -
LibraryAdapterがその橋渡しを行い、旧式のLegacyLibraryを新しい形式のModernLibraryに適合させます。 - クライアントは
LibraryAdapterを通じて旧式のライブラリを利用します。
ポイントまとめ
-
目的:
- 互換性のないインターフェイス(
LegacyLibrary)を、クライアントが利用可能な形式(ModernLibrary)に適合させる。
- 互換性のないインターフェイス(
-
コードの分離:
- 既存のクラス(
LegacyLibrary)に変更を加えず、LibraryAdapterを追加することで適応を実現。 - クライアント(
BookManager)はModernLibraryに依存しており、柔軟でテストしやすい設計になっています。
- 既存のクラス(
-
拡張性:
-
LibraryAdapterを拡張すれば、別のフォーマットの旧式ライブラリも適応可能。
-
Adapterパターンが用いられるケース
- レガシーシステムと新システムを統合する際
- サードパーティのライブラリを既存のシステムに組み込む場合
- 異なるデータ形式間の変換が必要な場合
まとめ
Adapterパターンは、互換性のないインターフェース間のギャップを埋めるための強力なツールです。既存のコードを変更せずに新しいシステムに統合できるため、システムの柔軟性と再利用性を高めることができます。