1. Home
  2. Golang
  3. DesignPatterns
  4. Go における Adapter パターン - インターフェースの適用と統一

Go における Adapter パターン - インターフェースの適用と統一

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

ソフトウェア開発では、異なるインターフェースを持つコンポーネント同士を統一的に扱いたい場面があります。そのような場合に役立つのが 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. アダプター(変換プラグ)

ここで変換プラグの出番です。以下に定義した構造体 PlugAdapterEuropePlug を実装しつつ、日本の電化製品 (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 を持っています。

ロギングライブラリメソッド名の違い使い方の違い
LogrusInfo(message string)直接メソッドを呼び出す
ZapLog(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 の統一やライブラリの置き換えなど、多くの場面で活用できます。

より柔軟でメンテナブルなコードを実現していきたいですね。

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級