依存性の注入
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を注入できます。本番環境では、実際の実装を注入します。どちらのシナリオでも、構造体は変更されません。
チャレンジ
簡単依存性注入の力を実証するロギングシステムを構築しましょう。このサービスは、ログが実際にどこへ送られるのかを知らなくても、また気にすることなく、さまざまな出力先にログを書き込めます。
コードを3つのファイルに分けて整理します。
logger.go:Log(message string) stringメソッドを必要とするLoggerインターフェースを定義します。次に、2種類の異なるロガー実装を作成します。ConsoleLoggerとPrefixフィールド。Logメソッドは[CONSOLE] [Prefix]: [message]を返しますFileLoggerとFilenameフィールド。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 を呼び出しているだけです。この柔軟性こそが依存性注入の本質です。同じサービスコードを、まったく異なるロギング実装で動作させられます。
自分で試してみよう
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復習 - ファイルパーサー自分で練習してみよう: Goオンラインコンパイラ