プログラムを開発していると、異なるインターフェースを持つクラスを統一的に扱いたい場面がよくあります。そんなときに役立つのが Adapter(アダプター)パターン です。
このパターンを活用すると、既存のクラスを変更せずに、異なるインターフェースを持つオブジェクトを統一的に扱えるようになります。
本記事では C# を用いて Adapter パターンの基本概念をわかりやすく解説します。他のプログラミング言語でも応用可能な考え方なので、ぜひ参考にしてください。
Adapter パターンとは?
Adapter(アダプター)パターン は、異なるインターフェースを持つクラスを統一的に扱うための 構造(Structural)パターン の一つです。
主に「既存のクラスを変更せずに、新しいインターフェースへ適応させる」ために使用されます。
Adapter パターンのイメージ
日常生活で考えてみると、コンセントの変換プラグ が Adapter パターンの良い例です。
例えば、日本の電化製品を海外で使用する場合、国によってコンセントの形状が異なるため、そのままでは使えません。
しかし、変換プラグ(Adapter)を使えば、日本の電化製品を海外のコンセントに適応させることができます。
このように、「既存の仕組みを変えずに、新しい環境で利用できるようにする」 のが Adapter パターンの役割です。
Adapter パターンのポイント
- 異なるインターフェースをつなぐ
- 既存のクラスと新しいインターフェースを統合するために Adapter クラスを作成する。
- 既存のクラスを変更せずに利用できる
- 元のクラスのコードを直接変更せずに、新しいインターフェースで動作させることができる。
- 外部ライブラリやレガシーコードの活用に最適
- 古いシステムと新しいシステムを統合するときや、異なる API やライブラリを統一的に扱いたい場合に役立つ。
Adapter パターンの利用イメージ(C# の簡単な例)
例えば、古いクラス OldSystem には OldMethod() というメソッドがあり、新しいシステムでは INewSystem の NewMethod() を使いたいとします。
このままでは OldSystem は INewSystem を実装していないため、新しいシステムと直接互換性がありません。
ここで Adapter パターンを使えば、OldSystem を INewSystem として扱えるように変換 できます。
// 旧システム(そのままでは新しいシステムと互換性がない)
public class OldSystem
{
public void OldMethod()
{
Console.WriteLine("旧システムのメソッドが実行されました");
}
}
// 新しいシステムのインターフェース
public interface INewSystem
{
void NewMethod();
}
// Adapter クラス(旧システムを新しいインターフェースに適応)
public class SystemAdapter : INewSystem
{
private readonly OldSystem _oldSystem;
public SystemAdapter(OldSystem oldSystem)
{
_oldSystem = oldSystem;
}
public void NewMethod()
{
_oldSystem.OldMethod(); // 旧システムのメソッドをラップ
}
}
// Adapter を使う側
public class Program
{
public static void Main()
{
OldSystem oldSystem = new OldSystem();
INewSystem adaptedSystem = new SystemAdapter(oldSystem);
adaptedSystem.NewMethod(); // 出力: 旧システムのメソッドが実行されました
}
}
このように、Adapter クラス SystemAdapter を用いることで、旧システムのメソッドを新しいインターフェースで利用できるように変換 できます。
Adapter パターンの種類
Adapter パターンには 「クラス Adapter」 と 「オブジェクト Adapter」 の 2 種類があります。
1. クラス Adapter(継承ベース)
- 「継承」を利用 して異なるインターフェースを適応させる方法。
- C# では多重継承ができないため、あまり使用されない。
2. オブジェクト Adapter(委譲ベース)
- 「委譲(composition)」を利用 して異なるインターフェースを適応させる方法。
- C# ではこの方法が一般的に使われる。
- 上記の
SystemAdapterはオブジェクト Adapter の例。
Adapter パターンが必要になる場面
Adapter パターンは、異なるインターフェースを統一的に扱う 必要があるときに使用されます。以下のような状況で特に有効です。
① 既存のクラスを変更せずに、新しいインターフェースへ適応したい
プロジェクトの途中でシステムの仕様が変わり、新しいインターフェースに統一する必要が生じることがあります。しかし、既存のクラスを直接変更すると、ほかのコードにも影響が出る可能性がある ため、直接変更できない場合があります。
例:古いシステムと新しいシステムの統合
- 旧システムの
OldLoggerクラス にはLogMessage()というメソッドがある。 - 新しいシステムの
INewLoggerインターフェース ではWriteLog()というメソッドが必要。 - 旧システムのコードを変更せずに
OldLoggerをINewLoggerとして扱いたい。
Adapter を使うことで、旧システムの OldLogger を新しい INewLogger として統合できます。
② 外部ライブラリの API が既存のシステムと互換性がない
プロジェクトで外部ライブラリを導入する際、そのライブラリの API のメソッド名やデータ形式が、自分のシステムと異なることがあります。
この場合、Adapter を使用して外部ライブラリのメソッドを 既存のシステムに適した形に変換 できます。
例:サードパーティの決済 API の統合
- 既存のシステムでは
ProcessPayment()というメソッドを使用。 - 新しく導入する外部ライブラリは
MakeTransaction()というメソッドを提供。 - Adapter を使い、
ProcessPayment()の呼び出しがMakeTransaction()を内部で実行するように変換すれば、コードの変更を最小限に抑えられる。
③ レガシーコードを新しいシステムに適応させたい
レガシーシステム(古いシステム)では、古いコーディング規約や独自の命名ルール が使われていることがあります。
新しいシステムに統合するときに、レガシーコードの構造をそのまま維持しつつ、新しいシステムの仕様に適応させる必要があります。
例:レガシーコードをモダナイズ
- レガシーシステムでは
LegacyDatabaseクラスがFetchData()メソッドを使ってデータを取得。 - 新しいシステムでは
INewDatabaseインターフェースにGetData()メソッドが必要。 - Adapter を作成すれば、既存の
LegacyDatabaseを新しいINewDatabaseとして扱えるようになる。
④ 異なるデータフォーマットを統一したい
異なるデータフォーマット(JSON, XML, CSV など)を扱うシステムでは、
異なるフォーマットを統一的に処理できるようにする ために Adapter を利用できます。
例:異なるデータフォーマットの統一
- JSON を扱う
JsonDataProviderクラスのGetJsonData()メソッド。 - XML を扱う
XmlDataProviderクラスのGetXmlData()メソッド。 - どちらのデータも
IDataProviderインターフェースのGetData()メソッドで取得できるようにする Adapter を作成する。
⑤ 異なる UI コンポーネントを統一的に扱いたい
フロントエンド開発では、異なる UI ライブラリ(WPF, WinForms, Blazor など)を使うことがあります。
それらを統一的に扱うために Adapter を適用できます。
例:異なる UI フレームワークのボタンを統一
- WPF の
WpfButtonクラスはClickCommandを使用。 - WinForms の
WinFormsButtonクラスはOnClickイベントを使用。 - 共通の
IButtonインターフェースを用意し、それぞれのボタンを適応させる Adapter を作成することで、UI を統一的に扱える。
Adapter パターンは、以下のような状況で役立ちます。
✔ 既存のクラスを変更せずに、新しいインターフェースに適応したい
✔ 外部ライブラリの API が既存のシステムと互換性がない
✔ レガシーコードを新しいシステムに適応させたい
✔ 異なるデータフォーマットを統一的に処理したい
✔ 異なる UI コンポーネントを共通のインターフェースで扱いたい
C# での Adapter パターンの実装例
C# で Adapter パターンを実装する方法を具体的に説明します。ここでは、既存の旧システムのクラスを、新しいシステムのインターフェースに適応させる Adapter を作成します。
📝 シナリオ
- 既存のシステム(旧システム)では
OldSystemクラスのOldMethod()を使う - 新しいシステムでは
INewSystemインターフェースのNewMethod()を使用 OldSystemを直接変更せずに、新しいシステムで利用できるようにする
(1) 旧システム(変更できないクラス)
まずは、旧システムのクラスを定義します。
// 既存の旧システム(変更したくないクラス)
public class OldSystem
{
public void OldMethod()
{
Console.WriteLine("旧システムのメソッドが実行されました");
}
}
このクラスは既存のシステムで動作しているため、直接変更することはできません。
(2) 新しいシステムのインターフェース
次に、新しいシステムで求められているインターフェースを定義します。
// 新しいシステムが求めるインターフェース
public interface INewSystem
{
void NewMethod();
}
新しいシステムでは NewMethod() というメソッドを使うことが前提になっています。
(3) Adapter クラスを作成
旧システムの OldSystem を、新しいインターフェース INewSystem に適合させる Adapter を作成します。
// Adapter クラス(旧システムを新しいインターフェースに適合させる)
public class SystemAdapter : INewSystem
{
private readonly OldSystem _oldSystem;
// コンストラクタで旧システムのインスタンスを受け取る
public SystemAdapter(OldSystem oldSystem)
{
_oldSystem = oldSystem;
}
// INewSystem のメソッドを実装
public void NewMethod()
{
// 旧システムのメソッドをラップして新しいインターフェースに適合
_oldSystem.OldMethod();
}
}
この SystemAdapter クラスは、新しいシステムの INewSystem インターフェースを実装しながら、
内部で OldSystem の OldMethod() を呼び出す ことで、旧システムのメソッドを変換 しています。
(4) Adapter を利用するコード
最後に、Adapter を使って 新しいシステムのインターフェースとして動作させる コードを記述します。
public class Program
{
public static void Main()
{
// 旧システムのインスタンスを作成
OldSystem oldSystem = new OldSystem();
// Adapter を使って新しいインターフェースに適合させる
INewSystem adaptedSystem = new SystemAdapter(oldSystem);
// 新しいシステムのメソッドを呼び出す
adaptedSystem.NewMethod(); // 「旧システムのメソッドが実行されました」と出力
}
}
このコードでは、旧システムの OldSystem を 直接変更せず に、新しい INewSystem インターフェースとして動作させています。
📌 Adapter パターンのポイント
SystemAdapterはINewSystemを実装し、旧システムのメソッドを適応させる- 既存の
OldSystemを直接変更せずに、新しいインターフェースと統合 - 「継承」ではなく「委譲(composition)」を利用しているため、柔軟性が高い
🎯 実際の開発での活用例
Adapter パターンは、実際の開発でもさまざまな場面で活用できます。
1️⃣ 外部ライブラリの統合
例えば、外部のサードパーティライブラリが新しい API 仕様と合わない場合に、Adapter を利用して統一することができます。
// 外部ライブラリのクラス
public class ExternalLogger
{
public void WriteLog(string message)
{
Console.WriteLine("LOG: " + message);
}
}
// 統一したいインターフェース
public interface ILogger
{
void Log(string message);
}
// Adapter の実装
public class LoggerAdapter : ILogger
{
private readonly ExternalLogger _externalLogger;
public LoggerAdapter(ExternalLogger externalLogger)
{
_externalLogger = externalLogger;
}
public void Log(string message)
{
_externalLogger.WriteLog(message);
}
}
この Adapter によって、外部ライブラリ ExternalLogger を、独自の ILogger インターフェースで統一 できます。
2️⃣ 異なるデータフォーマットの変換
例えば、CSV ファイルと JSON ファイルの読み込み処理を統一する場合も Adapter が使えます。
// CSV データを扱うクラス
public class CsvReader
{
public string ReadCsvData()
{
return "CSVデータを取得しました";
}
}
// 統一したいインターフェース
public interface IDataReader
{
string ReadData();
}
// Adapter を作成
public class CsvAdapter : IDataReader
{
private readonly CsvReader _csvReader;
public CsvAdapter(CsvReader csvReader)
{
_csvReader = csvReader;
}
public string ReadData()
{
return _csvReader.ReadCsvData();
}
}
このように、異なるデータフォーマットを 共通の IDataReader インターフェース で統一できます。
Adapter パターンのメリット・デメリット
1️⃣ 既存のコードを変更せずに新しいインターフェースへ適応できる
Adapter パターンの最大のメリットは、既存のコードを一切変更せずに、新しいシステムに統合できる ことです。
レガシーコードや外部ライブラリを変更できない場合でも、新しいインターフェースに適応できます。
例:
- 旧システムの
OldSystemクラスがOldMethod()を提供。- Adapter を利用することで、新しいシステムの
INewSystem.NewMethod()で呼び出せるようになる。
2️⃣ 外部ライブラリや API の統合が容易になる
外部のライブラリや API を導入する際、既存のシステムとインターフェースが異なる場合があります。
Adapter を使うことで、異なる仕様のライブラリを自分のシステムの仕様に適応させる ことができます。
例:
- サードパーティのログシステム (
ExternalLogger.WriteLog()) を、自社システムのログインターフェースILogger.Log()に統一する。
3️⃣ 異なるデータフォーマットを統一できる
データの形式が異なる場合も、Adapter を利用することで統一的に扱えるようになります。
例:
CsvReader.ReadCsvData()(CSV) とJsonReader.ReadJsonData()(JSON) を、共通のIDataReader.ReadData()で扱う。
4️⃣ コードの再利用性が向上する
Adapter を用意することで、異なるシステム間でコードを統一的に利用できる ため、再利用性が向上します。
また、システム全体の変更が必要になった場合でも、Adapter の変更だけで対応できる場合があります。
5️⃣ システムの拡張性が向上する
Adapter を使用すれば、既存のコードを変更せずに、新しいインターフェースを導入しやすくなる ため、拡張しやすいシステムを構築できます。
後から新しい機能やライブラリを追加するときにも適用しやすくなります。
⚠️ デメリット(Adapter パターンの注意点)
1️⃣ Adapter クラスを作成する手間が増える
Adapter を導入すると、Adapter クラスを別途作成する必要があるため、開発の手間が増える というデメリットがあります。
特に、適応するクラスが多い場合、各クラスに対する Adapter を作成する手間が増大 する可能性があります。
例:
- 異なる API を多数統合する場合、それぞれの API に対応した Adapter を作成しなければならない。
2️⃣ システムの複雑性が増す
Adapter パターンを多用しすぎると、コードの構造が複雑になり、可読性が低下 することがあります。
適用範囲を慎重に見極める必要があります。
例:
- たくさんの Adapter クラスが増えると、クラス間の依存関係が複雑になり、保守が困難になる。
3️⃣ 過剰に使用するとパフォーマンスに影響を与えることがある
Adapter は一種の「ラッパー」 なので、処理のオーバーヘッドが増える可能性があります。
特にリアルタイム性が求められるシステムでは、Adapter の呼び出し回数が増えるとパフォーマンスに悪影響を与える ことがあります。
例:
- 毎回データの変換が発生する処理で Adapter を適用すると、パフォーマンスが低下する可能性がある。
4️⃣ Adapter の依存関係が増える
Adapter クラスが増えることで、システム内の依存関係が増えてしまう ことがあります。
特に 複数の Adapter を組み合わせると、どのクラスがどの Adapter に依存しているのか管理が難しくなる ことがあります。
例:
- 旧システム →
AdapterA→AdapterB→ 新システム という形で Adapter を重ねて使用すると、管理が煩雑になる。
📌 Adapter パターンの適用を判断するポイント
Adapter パターンのメリットとデメリットを踏まえ、以下のようなケースでは Adapter の利用を検討するとよいでしょう。
🔹 適用すべきケース
✅ 既存のクラスを変更できないが、新しいインターフェースで統一したい
✅ 外部ライブラリや API の仕様を統一的に扱いたい
✅ 異なるデータフォーマット(JSON, XML, CSV)を統一して扱いたい
✅ システムの拡張性を向上させたい
❌ 適用を避けるべきケース
⚠ Adapter を大量に作成しなければならず、コードが煩雑になりそうな場合
⚠ パフォーマンスが重要で、Adapter を挟むことで処理のオーバーヘッドが問題になる場合
⚠ 適用するメリットが少なく、単純なクラスの変更で対応できる場合
まとめ
🔹 Adapter パターンとは?
- 異なるインターフェースを統一的に扱う ためのデザインパターン。
- 既存のコードを変更せずに、新しいシステムや API と統合できる。
- 「コンセントの変換プラグ」 のように、異なる形のものを適応させるイメージ。
🔹 Adapter パターンが必要になる場面
✅ 既存のクラスを変更せずに、新しいインターフェースに適応したい
✅ 外部ライブラリや API が既存のシステムと互換性がない
✅ レガシーコードを新しいシステムに統合したい
✅ 異なるデータフォーマット(JSON, XML, CSV)を統一したい
✅ 異なる UI コンポーネント(WPF, WinForms)を共通の操作で扱いたい
🔹 Adapter パターンのメリット
✔ 既存のコードを変更せずに、新しいインターフェースへ適応できる
✔ 外部ライブラリや API の統合が容易になる
✔ 異なるデータフォーマットを統一的に処理できる
✔ コードの再利用性・拡張性が向上する
🔹 Adapter パターンのデメリット
⚠ Adapter クラスを作成する手間が増える
⚠ システムの複雑性が増し、可読性が低下する可能性がある
⚠ 処理のオーバーヘッドが発生する可能性がある(パフォーマンス影響)
⚠ Adapter の依存関係が増え、管理が煩雑になることがある
🔹 Adapter パターンの適用を判断するポイント
✅ 異なる API やライブラリを共通のインターフェースで扱いたい
✅ 既存のコードを変更せずに統合したい
❌ 過剰に使用するとコードが複雑になりすぎるため注意
❌ パフォーマンスが重要な場合は、Adapter を挟む影響を考慮する
Adapter パターンは、異なるシステムや API をスムーズに統合するための強力なデザインパターンです。
適用すれば、既存のコードを変更せずに、新しいインターフェースへ適応できる ようになります。
特に 外部ライブラリの統合やデータフォーマットの変換、UI の統一 などに有効なので、
実際の開発で適用しながら、適切な使いどころを見極めましょう!
