Menu
Coddy logo textTech

依存性の注入

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

依存性注入とは、構造体が依存関係を内部で作成するのではなく、外部から受け取る手法です。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"))  // メール: 注文が完了しました: Book
    
    smsService := NewOrderService(SMSNotifier{})
    fmt.Println(smsService.PlaceOrder("Phone"))   // SMS: 注文が完了しました: Phone
}

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

challenge icon

チャレンジ

簡単

依存性注入の力を実証するロギングシステムを構築しましょう。このサービスは、ログが実際にどこへ送られるのかを知らなくても、また気にすることなく、さまざまな出力先にログを書き込めます。

コードを3つのファイルに分けて整理します。

  • logger.go: Log(message string) string メソッドを必要とする Logger インターフェースを定義します。次に、2種類の異なるロガー実装を作成します。
    • ConsoleLoggerPrefix フィールド。Log メソッドは [CONSOLE] [Prefix]: [message] を返します
    • FileLoggerFilename フィールド。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 を呼び出しているだけです。この柔軟性こそが依存性注入の本質です。同じサービスコードを、まったく異なるロギング実装で動作させられます。

自分で試してみよう

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腕試し

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

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

自分で練習してみよう: Goオンラインコンパイラ