依存性の注入
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 は変更されません。
チャレンジ
簡単依存性の注入(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を含めます。AppServiceにDoWork(task string) stringというメソッドを作成し、注入されたロガーを使用してProcessing: [task]というメッセージをログに記録し、ロガーが返した値をそのまま返します。main.go: 入力から設定を読み取り、両方のロガー型を作成し、それぞれを別々のAppServiceインスタンスに注入して、提供されたタスクで各サービスのDoWorkを呼び出します。各サービス呼び出しの結果をプリントします。
以下の入力が提供されます:
- 1行目: コンソールロガーのプレフィックス
- 2行目: ファイルロガーのファイル名
- 3行目: 処理するタスク
例えば、INFO、app.log、user authentication が与えられた場合、出力は以下のようになります:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationAppService が、コンソールにログを出力しているのかファイルに出力しているのかを関知していないことに注目してください。単に注入されたロガーの 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")
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー