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

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

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

ソフトウェア開発では、異なるインターフェースを持つコンポーネント同士を統一的に扱いたい場面があります。そのような場合に役立つのが Adapter(アダプター)パターン です。

本記事では、Adapter パターンの基本概念を理解しつつ、Go 言語での具体的な実装方法を紹介します。

contents

  1. Adapter パターン
    1. Adapter パターンのイメージ
  2. Go における Adapter パターンの実装
    1. 1. 既存のインターフェース(日本の電化製品)
    2. 2. 新しいインターフェース(ヨーロッパのコンセント)
    3. 3. アダプター(変換プラグ)
    4. 4. 変換プラグを使って日本の電化製品をヨーロッパで使用する
  3. Adapter パターンの実践例: ロギングライブラリの統一
    1. 1. 共通のインターフェースの定義
    2. 2. 既存のロギングライブラリの実装
    3. 3. Adapter の実装
    4. 4. Adapter を利用する

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

rito

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