Dependency Injection
Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy. Lektion 45 von 107.
Dependency Injection ist eine Technik, bei der eine Struktur ihre Abhängigkeiten von außen erhält, anstatt sie intern zu erstellen. In Go machen Interfaces dieses Muster natürlich und leistungsfähig.
Anstatt eine bestimmte Implementierung innerhalb einer Struktur fest zu codieren, akzeptierst du ein Interface. Dadurch kannst du Implementierungen austauschen, ohne den Code der Struktur zu ändern:
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
}
Erstelle nun eine Struktur, die von der Schnittstelle und nicht von einem konkreten Typ abhängt:
type OrderService struct {
notifier Notifier // Abhängigkeit über Interface injiziert
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
Der OrderService weiß nicht und kümmert sich nicht darum, ob er E-Mail oder SMS verwendet. Du injizierst die Abhängigkeit beim Erstellen des Dienstes:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Bestellung aufgegeben: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Bestellung aufgegeben: Phone
}
Dieser Ansatz macht deinen Code flexibler und besser testbar. Während des Testens kannst du einen Mock-Notifier einschleusen, der tatsächlich keine Nachrichten sendet. In der Produktion schleust du die echte Implementierung ein. Die Struktur bleibt in beiden Szenarien unverändert.
Aufgabe
EinfachErstellen wir ein Protokollierungssystem, das die Leistungsfähigkeit der Abhängigkeitsinjektion demonstriert. Du erstellst einen Dienst, der Protokolle an verschiedene Ziele schreiben kann, ohne dass der Dienst weiß oder sich darum kümmern muss, wohin diese Protokolle tatsächlich gehen.
Du organisierst deinen Code über drei Dateien:
logger.go: Definiere einLogger-Interface, das eineLog(message string) string-Methode erfordert. Erstelle dann zwei verschiedene Logger-Implementierungen:ConsoleLoggermit einemPrefix-Feld. SeineLog-Methode gibt[CONSOLE] [Prefix]: [message]zurückFileLoggermit einemFilename-Feld. SeineLog-Methode gibt[FILE:[Filename]] [message]zurück
service.go: Erstelle eineAppService-Struktur, die vomLogger-Interface abhängt (nicht von einem konkreten Typ). Füge eine KonstruktorfunktionNewAppServicehinzu, die einenLoggerakzeptiert und einen Zeiger aufAppServicezurückgibt. GibAppServiceeine Methode namensDoWork(task string) string, die den injizierten Logger verwendet, um die NachrichtProcessing: [task]zu protokollieren, und alles zurückgibt, was der Logger zurückgibt.main.go: Lies die Konfiguration aus der Eingabe, erstelle beide Logger-Typen, injiziere jeden von ihnen in eine separateAppService-Instanz und rufe bei jedem DienstDoWorkmit der bereitgestellten Aufgabe auf. Gib das Ergebnis jedes Dienstaufrufs aus.
Die folgenden Eingaben werden bereitgestellt:
- Zeile 1: Präfix des Konsolenloggers
- Zeile 2: Dateiname des Dateiloggers
- Zeile 3: Zu verarbeitende Aufgabe
Wenn beispielsweise INFO, app.log und user authentication angegeben werden, sollte deine Ausgabe folgendermaßen aussehen:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationBeachte, dass AppService nicht weiß, ob in eine Konsole oder in eine Datei protokolliert wird. Es ruft einfach Log für den injizierten Logger auf. Diese Flexibilität ist das Wesentliche der Abhängigkeitsinjektion: Derselbe Dienstcode funktioniert mit völlig unterschiedlichen Protokollierungsimplementierungen.
Probier es selbst
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Konsolen-Logger-Präfix lesen
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// Datei-Logger-Dateinamen lesen
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// Zu verarbeitende Aufgabe lesen
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: Erstelle einen ConsoleLogger mit dem Präfix
// TODO: Erstelle einen FileLogger mit dem Dateinamen
// TODO: Erstelle einen AppService mit dem injizierten ConsoleLogger
// TODO: Erstelle einen weiteren AppService mit dem injizierten FileLogger
// TODO: Rufe DoWork auf jedem Service mit der Aufgabe auf und gib die Ergebnisse aus
fmt.Println("result1")
fmt.Println("result2")
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP in Go
Externe DateienGo-Workspace & ModulePackages & ImportsExportierte vs. nicht exportierte NamenEinführung in OOP mit GoStructs als KlassenMethoden für Structs definierenPointer- vs. Value-ReceiverStruct-InitialisierungKonstruktorfunktionenRückblick – Einfacher Taschenrechner4Schnittstellen
Einführung in SchnittstellenImplizite ImplementierungSchnittstelle als VertragLeere Schnittstelle (any)TypzusicherungTypwechselZusammensetzung von SchnittstellenStringer- und Error-SchnittstellenRückblick – Formenrechner7Kapselung
Exportierte vs. nicht exportierte FelderKapselung auf PaketebeneGetter- und Setter-MethodenInformationsverbergung in GoRückblick – Studierendendatensätze10Generics (Go 1.18+)
Einführung in GenericsTypparameterTypbeschränkungenGenerische StrukturenWorkaround für generische MethodenZusammenfassung – Generische Sammlung2Typen & Structs im Detail
Grundlegende & zusammengesetzte TypenBenutzerdefinierte TypdefinitionenStruct-TagsAnonyme StructsVerschachtelte StructsNullwerte & StandardwerteRückblick – Kontaktbuch5Komposition statt Vererbung
Warum Go keine Vererbung hatGrundlagen der Struct-EinbettungMethoden-PromotionMehrere Structs einbettenEinbettung vs. AggregationVerbergen eingebetteter MethodenRückblick – Mitarbeiterhierarchie8Fehlerbehandlung & OOP
Das Error-InterfaceBenutzerdefinierte FehlertypenError-Wrapping (fmt.Errorf)Sentinel-Fehlererrors.Is() und errors.As()Panic, Defer und RecoverRückblick – Dateiparser3Zeiger & Speicher
Grundlagen von Zeigern in GoZeiger auf StructsÜbergabe per Wert vs. ReferenzDie Funktion new()Garbage Collection in GoRückblick – Verkettete Liste erstellen6Polymorphismus in Go
Polymorphismus über InterfacesDuck-Typing in GoRegeln zur Interface-ErfüllungPolymorphe SammlungenDependency InjectionZusammenfassung – Zahlungsprozessor9Konkurrenz & OOP
Grundlagen der GoroutinesChannels & KommunikationGepufferte vs. ungepufferte ChannelsSelect-Anweisungsync.Mutex & sync.RWMutexsync.WaitGroupThread-sicheres Struct-DesignRückblick – Worker PoolÜbe selbstständig: Online-Go-Compiler