Dependency Injection in Go: für Anfänger
Dependency Injection (DI), oder Abhängigkeitsinjektion, ist eines der wichtigsten Entwurfsmuster, das hilft, sauberen, testbaren und wartbaren Code zu schreiben. In der Welt von Go, wo Einfachheit und Klarheit am höchsten geschätzt werden, ermöglicht DI die Vermeidung von „gottgleichen Objekten“ und starren Kopplungen zwischen Komponenten. In diesem Artikel werden wir untersuchen, was Dependency Injection in Go ist, warum sie benötigt wird und wie man sie in der Praxis mit einfachen und verständlichen Beispielen anwendet.
Was ist Dependency Injection?
Stellen Sie sich vor, Sie schreiben ein Programm, das Willkommens-E-Mails an Benutzer sendet. Ihr Code würde wahrscheinlich so aussehen:
package main
import "fmt"
type EmailService struct{}
func (e *EmailService) SendWelcomeEmail(userEmail string) { fmt.Printf("Sende E-Mail an %s", userEmail)}
type UserController struct { emailService *EmailService}
func NewUserController() *UserController { return &UserController{ emailService: &EmailService{}, // Starre Kopplung }}
func (uc *UserController) RegisterUser(email string) { // ... Registrierungslogik uc.emailService.SendWelcomeEmail(email)}
func main() { controller := NewUserController() controller.RegisterUser("user@example.com")}Das Problem an diesem Code ist, dass UserController selbst eine Instanz von EmailService erstellt. Dies nennt man starre Kopplung. Wenn Sie EmailService durch z. B. SmsService ersetzen oder für Tests ein Mock-Objekt verwenden möchten, müssen Sie den Code innerhalb von UserController ändern.
Dependency Injection löst dieses Problem. Anstatt Abhängigkeiten innerhalb der Struktur zu erstellen, „injizieren“ wir sie von außen. Die Grundidee: „Frage nicht, was du brauchst. Sage, was du brauchst, und erhalte es fertig.“
Manuelle Abhängigkeitsinjektion über den Konstruktor
Die einfachste und gebräuchlichste Methode der DI in Go ist die Übergabe von Abhängigkeiten über eine Konstruktorfunktion. Lassen Sie uns unser Beispiel umschreiben:
package main
import "fmt"
// Definieren eines Interfaces für Flexibilitättype Notifier interface { Send(to string, message string) error}
type EmailService struct{}
func (e *EmailService) Send(to string, message string) error { fmt.Printf("E-Mail an %s: %s", to, message) return nil}
type SmsService struct{}
func (s *SmsService) Send(to string, message string) error { fmt.Printf("SMS an %s: %s", to, message) return nil}
type UserController struct { notifier Notifier // Abhängigkeit über Interface}
// NewUserController – Konstruktor, der die Abhängigkeit entgegennimmtfunc NewUserController(n Notifier) *UserController { return &UserController{ notifier: n, }}
func (uc *UserController) RegisterUser(email string) { // ... Geschäftslogik uc.notifier.Send(email, "Willkommen!")}
func main() { // Abhängigkeit manuell injizieren emailNotifier := &EmailService{} controller := NewUserController(emailNotifier) controller.RegisterUser("user@example.com")
// Einfach die Implementierung wechseln smsNotifier := &SmsService{} controller2 := NewUserController(smsNotifier) controller2.RegisterUser("+123456789")}Was hat sich geändert?
- Wir haben ein Interface
Notifierdefiniert. Jetzt weißUserControllernicht, welcher Dienst genau verwendet wird – E-Mail oder SMS. - Der Konstruktor
NewUserControllernimmtNotifierals Argument entgegen. Das ist die Abhängigkeitsinjektion. - In
mainentscheiden wir selbst, welche Implementierung wir übergeben.
Dieser Ansatz macht den Code flexibel, testbar und entspricht dem Prinzip der Inversion of Control.