Go における Adapter パターン - インターフェースの適用と統一
- 公開日
- カテゴリ:DesignPatterns
- タグ:Golang

ソフトウェア開発では、異なるインターフェースを持つコンポーネント同士を統一的に扱いたい場面があります。そのような場合に役立つのが Adapter(アダプター)パターン です。
本記事では、Adapter パターンの基本概念を理解しつつ、Go 言語での具体的な実装方法を紹介します。
Adapter パターン
Adapter パターンは、「あるインターフェースを、別のインターフェースに適用させる」ためのデザインパターンです。既存のコードを変更せずに、新しいインターフェースで利用できるようにするのが特徴です。
Adapter パターンを適用すると、既存のコードを変更せずに新しい規格で利用可能にしたり、異なるライブラリや API を統一できたり、古いシステムと新しいシステムをつないだりできます。
Adapter パターンのイメージ
Adapter パターンの考え方を、日常的な例に当てはめると「海外のコンセントと変換プラグ」がわかりやすいです。
- 日本の電化製品(100V, Aタイプのプラグ) を ヨーロッパのコンセント(220V, Cタイプのソケット) に挿したい。
- 直接は使えないので 変換プラグ(アダプター) を使う。
このように、異なる規格を持つものを適用させるのが Adapter パターンの役割です。
Go における Adapter パターンの実装
先ほどイメージしたプラグを実装に落とし込んでみます。
1. 既存のインターフェース(日本の電化製品)
日本の電化製品は A タイプのプラグを使用し、日本の 100V のコンセントに接続することを前提としています。以下の実装では、日本の電化製品である JapaneseDevice
構造体が JapanPlug
インターフェースを実装しており、InsertIntoJapanSocket()
メソッドを持っています。つまりこの電化製品は、日本の規格で使われることを前提としている実装になっています。
package main
import "fmt"
// JapanPlug 日本の電化製品のインターフェース
type JapanPlug interface {
InsertIntoJapanSocket()
}
// JapaneseDevice 日本の電化製品
type JapaneseDevice struct{}
func (d *JapaneseDevice) InsertIntoJapanSocket() {
fmt.Println("コンセントに接続しました。")
}
2. 新しいインターフェース(ヨーロッパのコンセント)
一方でヨーロッパのコンセントは C タイプのプラグ を使用し、220V の電圧で動作します。以下のインターフェース EuropePlug
は、ヨーロッパの電化製品が InsertIntoEuropeSocket()
を実装することを前提としています。
// EuropePlug ヨーロッパの電化製品のインターフェース
type EuropePlug interface {
InsertIntoEuropeSocket()
}
つまり、日本の電化製品(JapaneseDevice構造体)では、プラグの形状が違う(EuropePlug インターフェースを実装していない)ので、ヨーロッパのコンセントには接続できないわけですね。
3. アダプター(変換プラグ)
ここで変換プラグの出番です。以下に定義した構造体 PlugAdapter
は EuropePlug
を実装しつつ、日本の電化製品 (JapaneseDevice
) を内部で使用します。InsertIntoEuropeSocket()
が呼ばれると、JapaneseDevice.InsertIntoJapanSocket()
を内部で実行し、日本の電化製品がヨーロッパのコンセントで使えるようになります。
// PlugAdapter EuropePlug を JapanPlug に適合させる
type PlugAdapter struct {
JapaneseDevice *JapaneseDevice
}
// InsertIntoEuropeSocket EuropePlug のインターフェースを満たす
func (pa *PlugAdapter) InsertIntoEuropeSocket() {
fmt.Println("変換プラグを使用中...")
pa.JapaneseDevice.InsertIntoJapanSocket()
}
4. 変換プラグを使って日本の電化製品をヨーロッパで使用する
この PlugAdapter
を使用することで、日本の電化製品をヨーロッパのコンセントに適用させることができます。
func main() {
japanDevice := &JapaneseDevice{}
adapter := &PlugAdapter{JapaneseDevice: japanDevice}
// ヨーロッパのコンセント(EuropePlug)のインターフェースで、日本の電化製品を使う
adapter.InsertIntoEuropeSocket()
}
// 変換プラグを使用中...
// コンセントに接続しました。
このように、Adapter パターンを利用することで、本来互換性のないインターフェース同士を橋渡しし、異なる規格のものを統一的に扱えるようになります。まるで通訳が異なる言語を話す人々の間を取り持つように、Adapter は異なるインターフェースの間でデータのやり取りを可能にします。Go における Adapter パターンの実装では、特定のインターフェースに適合しない既存の構造体を、新しい環境で使えるように変換する仕組みを提供することで、コードの再利用性と柔軟性を向上させることができます。
Adapter パターンの実践例: ロギングライブラリの統一
Adapter パターンは、異なるインターフェースを統一するために非常に便利です。実際の開発では、異なるライブラリや外部 API を統一的に扱いたい状況があるかもしれません。例えば、Go には Logrus や Zap など複数のロギングライブラリがあり、それぞれ異なる API を持っています。
ロギングライブラリ | メソッド名の違い | 使い方の違い |
---|---|---|
Logrus | Info(message string) | 直接メソッドを呼び出す |
Zap | Log(level string, message string) | ログレベルを引数で指定する |
例: Logrus のコード
logrus := LogrusLogger{}
logrus.Info("アプリケーションが起動しました")
logrus.Error("エラーが発生しました")
例: Zap のコード
zap := ZapLogger{}
zap.Log("INFO", "アプリケーションが起動しました")
zap.Log("ERROR", "エラーが発生しました")
このままでは、アプリケーションで異なるロガーを利用する際に、コードの変更が必要になります。そこで、Adapter パターンを使って両者を統一し、共通のインターフェースで扱えるようにします。
1. 共通のインターフェースの定義
まず、すべてのロギングライブラリを統一する 共通のインターフェース Logger
を定義します。
// Logger 共通のロギングインターフェース
type Logger interface {
Info(message string)
Error(message string)
}
2. 既存のロギングライブラリの実装
次に、異なるロギングライブラリを定義します。
package main
import "fmt"
// LogrusLogger ログライブラリ A の実装
type LogrusLogger struct{}
func (l *LogrusLogger) Info(message string) {
fmt.Println("[Logrus INFO]:", message)
}
func (l *LogrusLogger) Error(message string) {
fmt.Println("[Logrus ERROR]:", message)
}
// ZapLogger ログライブラリ B の実装
type ZapLogger struct{}
func (z *ZapLogger) Log(level string, message string) {
fmt.Printf("[Zap %s]: %s\n", level, message)
}
3. Adapter の実装
各ロギングライブラリを Logger
インターフェースに適合させる Adapter を実装します。
// LogrusAdapter Logrus を共通インターフェースに適用
type LogrusAdapter struct {
logrus *LogrusLogger
}
func (l *LogrusAdapter) Info(message string) {
l.logrus.Info(message)
}
func (l *LogrusAdapter) Error(message string) {
l.logrus.Error(message)
}
// ZapAdapter Zap を共通インターフェースに適用
type ZapAdapter struct {
zap *ZapLogger
}
func (z *ZapAdapter) Info(message string) {
z.zap.Log("INFO", message)
}
func (z *ZapAdapter) Error(message string) {
z.zap.Log("ERROR", message)
}
4. Adapter を利用する
Adapter を利用すると、異なるロギングライブラリを Logger
インターフェースを通じて統一的に扱うことができます。
func main() {
// 異なるログシステムを統一
logrusAdapter := &LogrusAdapter{logrus: &LogrusLogger{}}
zapAdapter := &ZapAdapter{zap: &ZapLogger{}}
// 共通の Logger インターフェースを利用
loggers := []Logger{logrusAdapter, zapAdapter}
for _, logger := range loggers {
logger.Info("アプリケーションが起動しました")
logger.Error("エラーが発生しました")
}
}
出力結果:
[Logrus INFO]: アプリケーションが起動しました
[Logrus ERROR]: エラーが発生しました
[Zap INFO]: アプリケーションが起動しました
[Zap ERROR]: エラーが発生しました
このように、Adapter パターンを使うことで 異なるロギングライブラリの API を統一し、アプリケーションのコードを変更せずに扱えるようになりました。
- Adapter パターンを使うことで、異なるロギングライブラリの API を統一できた
- Logger インターフェースを定義し、LogrusAdapter と ZapAdapter を実装することで、異なるライブラリを一貫した方法で扱えるようになった
- Adapter パターンを適用することで、アプリケーションのコードを変更せずにロギングライブラリを差し替えたり、追加したりできる
まとめ
Adapter パターンの基本概念と Go における実装方法をご紹介しました。Adapter パターンを適用すると、互換性のないインターフェースを統一し、異なるシステムやライブラリを柔軟に扱うことが可能になります。実際の開発では、外部 API の統一やライブラリの置き換えなど、多くの場面で活用できます。
より柔軟でメンテナブルなコードを実現していきたいですね。