Menu
Coddy logo textTech

依存性の注入

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 45/107。

依存性の注入 (Dependency Injection) は、構造体が依存関係を内部で作成するのではなく、外部から受け取る手法です。Goでは、インターフェースによってこのパターンを自然かつ強力に利用できます。

構造体の中に特定の実装をハードコーディングする代わりに、インターフェースを受け取ります。これにより、構造体のコードを変更することなく実装を入れ替えることができます:

type Notifier interface {
    Send(message string) string
}

type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
    return "Email: " + message
}

type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
    return "SMS: " + message
}

次に、具象型ではなくインターフェースに依存する構造体を作成します:

type OrderService struct {
    notifier Notifier  // インターフェースを介して注入される依存関係
}

func NewOrderService(n Notifier) *OrderService {
    return &OrderService{notifier: n}
}

func (o *OrderService) PlaceOrder(item string) string {
    return o.notifier.Send("Order placed: " + item)
}

OrderService は、メールを使用しているのか SMS を使用しているのかを認識したり、気にしたりすることはありません。サービスを作成する際に依存関係を注入します:

func main() {
    emailService := NewOrderService(EmailNotifier{})
    fmt.Println(emailService.PlaceOrder("Book"))  // Email: 注文が確定しました: Book
    
    smsService := NewOrderService(SMSNotifier{})
    fmt.Println(smsService.PlaceOrder("Phone"))   // SMS: 注文が確定しました: Phone
}

このアプローチにより、コードの柔軟性とテストのしやすさが向上します。テスト時には、実際にはメッセージを送信しないモックの notifier を注入できます。本番環境では、実際の(本物の)実装を注入します。どちらのシナリオでも、struct は変更されません。

challenge icon

チャレンジ

簡単

依存性の注入(Dependency Injection)の威力を示すロギングシステムを構築しましょう。ログが実際にどこに出力されるかをサービス自体が知ることなく、異なる出力先にログを書き込むことができるサービスを作成します。

コードは以下の3つのファイルに分けて構成します:

  • logger.go: Log(message string) string メソッドを必要とする Logger インターフェースを定義します。次に、2つの異なるロガーの実装を作成します:
    • Prefix フィールドを持つ ConsoleLogger — その Log メソッドは [CONSOLE] [Prefix]: [message] を返します。
    • Filename フィールドを持つ FileLogger — その Log メソッドは [FILE:[Filename]] [message] を返します。
  • service.go: (具体的な型ではなく)Logger インターフェースに依存する AppService 構造体を作成します。Logger を受け取り AppService へのポインタを返すコンストラクタ関数 NewAppService を含めます。AppServiceDoWork(task string) string というメソッドを作成し、注入されたロガーを使用して Processing: [task] というメッセージをログに記録し、ロガーが返した値をそのまま返します。
  • main.go: 入力から設定を読み取り、両方のロガー型を作成し、それぞれを別々の AppService インスタンスに注入して、提供されたタスクで各サービスの DoWork を呼び出します。各サービス呼び出しの結果をプリントします。

以下の入力が提供されます:

  • 1行目: コンソールロガーのプレフィックス
  • 2行目: ファイルロガーのファイル名
  • 3行目: 処理するタスク

例えば、INFOapp.loguser authentication が与えられた場合、出力は以下のようになります:

[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authentication

AppService が、コンソールにログを出力しているのかファイルに出力しているのかを関知していないことに注目してください。単に注入されたロガーの Log を呼び出しているだけです。この柔軟性が依存性の注入の本質です。同じサービスコードが、完全に異なるロギング実装で動作します。

チートシート

依存性の注入(Dependency Injection)は、構造体が依存関係を内部で作成するのではなく、外部から受け取る手法です。Goでは、インターフェースによってこのパターンを自然かつ強力に実現できます。

依存関係のためのインターフェースを定義します:

type Notifier interface {
    Send(message string) string
}

インターフェースの具体的な実装を作成します:

type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
    return "Email: " + message
}

type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
    return "SMS: " + message
}

具体的な型ではなく、インターフェースに依存する構造体を作成します:

type OrderService struct {
    notifier Notifier  // インターフェースを介して依存性が注入される
}

func NewOrderService(n Notifier) *OrderService {
    return &OrderService{notifier: n}
}

func (o *OrderService) PlaceOrder(item string) string {
    return o.notifier.Send("Order placed: " + item)
}

サービスを作成する際に依存性を注入します:

emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book"))  // Email: Order placed: Book

smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone"))   // SMS: Order placed: Phone

このアプローチにより、構造体のコードを変更することなく異なる実装を入れ替えることができるため、コードの柔軟性とテストのしやすさが向上します。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// コンソールロガーのプレフィックスを読み込む
	prefix, _ := reader.ReadString('\n')
	prefix = prefix[:len(prefix)-1]
	
	// ファイルロガーのファイル名を読み込む
	filename, _ := reader.ReadString('\n')
	filename = filename[:len(filename)-1]
	
	// 処理するタスクを読み込む
	task, _ := reader.ReadString('\n')
	if len(task) > 0 && task[len(task)-1] == '\n' {
		task = task[:len(task)-1]
	}
	
	// TODO: プレフィックスを使用してConsoleLoggerを作成する
	
	// TODO: ファイル名を使用してFileLoggerを作成する
	
	// TODO: ConsoleLoggerを注入したAppServiceを作成する
	
	// TODO: FileLoggerを注入した別のAppServiceを作成する
	
	// TODO: 各サービスでDoWorkをタスクと共に呼び出し、結果を出力する
	fmt.Println("result1")
	fmt.Println("result2")
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン