Go Dependency Injection erklärt: Von Null zum Helden
Emily Parker
Product Engineer · Leapcell

Erkundung der Dependency Injection (DI) in Golang
Abstract
Dieser Artikel konzentriert sich auf Inhalte im Zusammenhang mit Dependency Injection (DI) in Golang. Zu Beginn wird das Konzept von DI anhand der typischen objektorientierten Sprache Java vorgestellt, um Anfängern ein verständliches Herangehen zu ermöglichen. Die Wissenspunkte in diesem Artikel sind relativ verstreut und behandeln die SOLID-Prinzipien der objektorientierten Programmierung sowie typische DI-Frameworks in verschiedenen Sprachen usw.
I. Einleitung
Im Bereich der Programmierung ist Dependency Injection ein wichtiges Designmuster. Das Verständnis seiner Anwendung in Golang ist von entscheidender Bedeutung für die Verbesserung der Codequalität, Testbarkeit und Wartbarkeit. Um DI in Golang besser zu erklären, beginnen wir zunächst mit der gängigen objektorientierten Sprache Java und führen das Konzept von DI ein.
II. Analyse des DI-Konzepts
(I) Gesamtbedeutung von DI
Dependency bedeutet, sich auf etwas zu verlassen, um Unterstützung zu erhalten. Beispielsweise sind Menschen in hohem Maße von Mobiltelefonen abhängig. Im Kontext der Programmierung bedeutet dies, dass Klasse A eine Abhängigkeit von Klasse B hat, wenn Klasse A bestimmte Funktionen von Klasse B verwendet. In Java ist es vor der Verwendung der Methoden einer anderen Klasse in der Regel erforderlich, ein Objekt dieser Klasse zu erstellen (d. h. Klasse A muss eine Instanz von Klasse B erstellen). Und der Prozess, die Aufgabe der Objekterstellung an andere Klassen zu übergeben und die Abhängigkeiten direkt zu nutzen, ist die „Dependency Injection“.
(II) Definition von Dependency Injection
Dependency Injection (DI) ist ein Designmuster und eines der Kernkonzepte des Spring Frameworks. Seine Hauptfunktion besteht darin, die Abhängigkeitsbeziehung zwischen Java-Klassen zu beseitigen, eine lose Kopplung zu erreichen und Entwicklung und Tests zu erleichtern. Um DI tiefgreifend zu verstehen, müssen wir zunächst die Probleme verstehen, die es zu lösen versucht.
III. Veranschaulichung allgemeiner Probleme und des DI-Prozesses anhand von Java-Codebeispielen
(I) Problem der engen Kopplung
In Java erstellen wir, wenn wir eine Klasse verwenden, normalerweise eine Instanz dieser Klasse, wie im folgenden Code gezeigt:
class Player{ Weapon weapon; Player(){ // Eng gekoppelt mit der Sword-Klasse this.weapon = new Sword(); } public void attack() { weapon.attack(); } }
Diese Methode hat das Problem einer zu engen Kopplung. Beispielsweise ist die Waffe des Spielers als Schwert (Sword) festgelegt, und es ist schwierig, sie durch eine Pistole (Gun) zu ersetzen. Wenn wir das Schwert in eine Pistole ändern wollen, muss der gesamte relevante Code geändert werden. Wenn der Codeumfang klein ist, ist dies möglicherweise kein großes Problem, aber wenn der Codeumfang groß ist, wird es viel Zeit und Energie in Anspruch nehmen.
(II) Dependency-Injection-Prozess (DI)
Dependency Injection ist ein Designmuster, das die Abhängigkeitsbeziehung zwischen Klassen eliminiert. Wenn beispielsweise Klasse A von Klasse B abhängt, erstellt Klasse A Klasse B nicht mehr direkt. Stattdessen wird diese Abhängigkeitsbeziehung in einer externen XML-Datei (oder Java-Konfigurationsdatei) konfiguriert, und der Spring-Container erstellt und verwaltet die Bean-Klasse gemäß den Konfigurationsinformationen.
class Player{ Weapon weapon; // weapon wird injiziert Player(Weapon weapon){ this.weapon = weapon; } public void attack() { weapon.attack(); } public void setWeapon(Weapon weapon){ this.weapon = weapon; } }
Im obigen Code wird die Instanz der Weapon-Klasse nicht innerhalb des Codes erstellt, sondern von außen über den Konstruktor übergeben. Der übergebene Typ ist die übergeordnete Klasse Weapon, so dass der übergebene Objekttyp eine beliebige Unterklasse von Weapon sein kann. Die zu übergebende spezifische Unterklasse kann in der externen XML-Datei (oder Java-Konfigurationsdatei) konfiguriert werden. Der Spring-Container erstellt eine Instanz der erforderlichen Unterklasse gemäß den Konfigurationsinformationen und injiziert sie in die Player-Klasse. Das Beispiel ist wie folgt:
<bean id="player" class="com.qikegu.demo.Player"> <construct-arg ref="weapon"/> </bean> <bean id="weapon" class="com.qikegu.demo.Gun"> </bean>
Im obigen Code verweist das ref von <construct-arg ref="weapon"/>
auf die Bean mit id="weapon"
, und der übergebene Waffentyp ist Gun. Wenn wir es in Schwert ändern wollen, können wir die folgende Änderung vornehmen:
<bean id="weapon" class="com.qikegu.demo.Sword"> </bean>
Es ist zu beachten, dass lose Kopplung nicht bedeutet, die Kopplung vollständig zu beseitigen. Klasse A hängt von Klasse B ab, und es besteht eine enge Kopplung zwischen ihnen. Wenn die Abhängigkeitsbeziehung in Klasse A geändert wird, die von der übergeordneten Klasse B0 von Klasse B abhängt, kann Klasse A unter der Abhängigkeitsbeziehung zwischen Klasse A und Klasse B0 jede Unterklasse von Klasse B0 verwenden. Zu diesem Zeitpunkt ist die Abhängigkeitsbeziehung zwischen Klasse A und den Unterklassen von Klasse B0 lose gekoppelt. Es ist ersichtlich, dass die technische Grundlage der Dependency Injection der Polymorphismusmechanismus und der Reflexionsmechanismus ist.
(III) Arten der Dependency Injection
- Konstruktorinjektion: Die Abhängigkeitsbeziehung wird über den Klassenkonstruktor bereitgestellt.
- Setter-Injektion: Der Injektor verwendet die Setter-Methode des Clients, um die Abhängigkeiten zu injizieren.
- Interface-Injektion: Die Abhängigkeit stellt eine Injektionsmethode bereit, um die Abhängigkeiten in jeden Client zu injizieren, der sie an sie übergibt. Der Client muss eine Schnittstelle implementieren, und die Setter-Methode dieser Schnittstelle wird verwendet, um die Abhängigkeiten zu empfangen.
(IV) Funktionen der Dependency Injection
- Objekte erstellen.
- Klären, welche Klassen welche Objekte benötigen.
- Alle diese Objekte bereitstellen. Wenn Änderungen an den Objekten vorgenommen werden, untersucht die Dependency Injection dies, und es sollte keine Auswirkungen auf die Klassen haben, die diese Objekte verwenden. Das heißt, wenn sich die Objekte in Zukunft ändern, ist die Dependency Injection dafür verantwortlich, die korrekten Objekte für die Klassen bereitzustellen.
(V) Inversion of Control - Das Konzept hinter Dependency Injection
Inversion of Control bedeutet, dass eine Klasse ihre Abhängigkeiten nicht statisch konfigurieren sollte, sondern von anderen Klassen extern konfiguriert werden sollte. Dies folgt dem fünften Prinzip von S.O.L.I.D - Klassen sollten von Abstraktionen und nicht von spezifischen Dingen abhängen (Vermeidung von Hardcoding). Gemäß diesen Prinzipien sollte sich eine Klasse auf die Erfüllung ihrer eigenen Verantwortlichkeiten konzentrieren, anstatt die Objekte zu erstellen, die zur Erfüllung ihrer Verantwortlichkeiten benötigt werden. Hier kommt die Dependency Injection ins Spiel, da sie die notwendigen Objekte für die Klasse bereitstellt.
(VI) Vorteile der Verwendung von Dependency Injection
- Erleichtert Unit-Tests.
- Da die Initialisierung der Abhängigkeitsbeziehung von der Injektorkomponente abgeschlossen wird, reduziert sie Boilerplate-Code.
- Erleichtert die Erweiterung der Anwendung.
- Hilft, lose Kopplung zu erreichen, was in der Anwendungsprogrammierung von entscheidender Bedeutung ist.
(VII) Nachteile der Verwendung von Dependency Injection
- Der Lernprozess ist etwas kompliziert, und eine übermäßige Verwendung kann zu Management- und anderen Problemen führen.
- Viele Kompilierungsfehler werden bis zur Laufzeit verzögert.
- Dependency-Injection-Frameworks werden normalerweise durch Reflexion oder dynamische Programmierung implementiert, was die Verwendung von IDE-Automatisierungsfunktionen wie „Referenzen suchen“, „Aufrufhierarchie anzeigen“ und sicheres Refactoring verhindern kann.
Sie können Dependency Injection selbst implementieren oder Bibliotheken oder Frameworks von Drittanbietern verwenden, um dies zu erreichen.
(VIII) Bibliotheken und Frameworks zur Implementierung von Dependency Injection
- Spring (Java)
- Google Guice (Java)
- Dagger (Java und Android)
- Castle Windsor (.NET)
- Unity (.NET)
- Wire (Golang)
IV. Verständnis von DI in Golang TDD
Während der Verwendung von Golang haben viele Menschen viele Missverständnisse über Dependency Injection. Tatsächlich hat Dependency Injection viele Vorteile:
- Ein Framework ist nicht unbedingt erforderlich.
- Es wird das Design nicht übermäßig verkomplizieren.
- Es ist leicht zu testen.
- Es kann hervorragende und allgemeine Funktionen schreiben.
Nehmen wir das Schreiben einer Funktion, um jemanden zu begrüßen, als Beispiel. Wir erwarten, das tatsächliche Drucken zu testen. Die anfängliche Funktion lautet wie folgt:
func Greet(name string) { fmt.Printf("Hello, %s", name) }
Das Aufrufen von fmt.Printf
druckt den Inhalt jedoch auf die Standardausgabe, und es ist schwierig, ihn mit einem Testframework zu erfassen. Zu diesem Zeitpunkt müssen wir die Abhängigkeit des Druckens injizieren (d. h. „übergeben“). Diese Funktion muss sich nicht darum kümmern, wo und wie gedruckt werden soll, daher sollte sie eine Schnittstelle anstelle eines bestimmten Typs empfangen. Auf diese Weise können wir durch Ändern der Implementierung der Schnittstelle den gedruckten Inhalt steuern und dann Tests durchführen.
Betrachten wir den Quellcode von fmt.Printf
, können wir sehen:
// Es gibt die Anzahl der geschriebenen Bytes und alle aufgetretenen Schreibfehler zurück. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) }
Innerhalb von Printf
wird nur os.Stdout
übergeben und Fprintf
aufgerufen. Betrachten wir weiter die Definition von Fprintf
:
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return }
Unter anderem ist io.Writer
definiert als:
type Writer interface { Write(p []byte) (n int, err error) }
io.Writer
ist eine häufig verwendete Schnittstelle zum „Ablegen von Daten an einem Ort“. Basierend darauf verwenden wir diese Abstraktion, um den Code testbar zu machen und eine bessere Wiederverwendbarkeit zu erzielen.
(I) Schreiben von Tests
func TestGreet(t *testing.T) { buffer := bytes.Buffer{} Greet(&buffer,"Leapcell") got := buffer.String() want := "Hello, Leapcell" if got != want { t.Errorf("got '%s' want '%s'", got, want) } }
Der Typ buffer
im Paket bytes
implementiert die Writer
-Schnittstelle. Im Test verwenden wir ihn als Writer
. Nach dem Aufrufen von Greet
können wir den geschriebenen Inhalt darüber überprüfen.
(II) Versuch, die Tests auszuführen
Beim Ausführen der Tests tritt ein Fehler auf:
./di_test.go:10:7: too many arguments in call to Greet
have (*bytes.Buffer, string)
want (string)
(III) Schreiben von minimiertem Code, damit die Tests ausgeführt werden, und Überprüfen der fehlgeschlagenen Testausgabe
Gemäß der Aufforderung des Compilers beheben wir das Problem. Die geänderte Funktion lautet wie folgt:
func Greet(writer *bytes.Buffer, name string) { fmt.Printf("Hello, %s", name) }
Zu diesem Zeitpunkt lautet das Testergebnis:
Hello, Leapcell di_test.go:16: got '' want 'Hello, Leapcell'
Der Test schlägt fehl. Beachten Sie, dass der name
gedruckt werden kann, die Ausgabe jedoch an die Standardausgabe geht.
(IV) Schreiben von ausreichend Code, damit er besteht
Verwenden Sie writer
, um die Begrüßung an den Puffer im Test zu senden. fmt.Fprintf
ähnelt fmt.Printf
. Der Unterschied besteht darin, dass fmt.Fprintf
einen Writer
-Parameter empfängt, um die Zeichenkette zu übergeben, während fmt.Printf
standardmäßig an die Standardausgabe ausgibt. Die geänderte Funktion lautet wie folgt:
func Greet(writer *bytes.Buffer, name string) { fmt.Fprintf(writer, "Hello, %s", name) }
Zu diesem Zeitpunkt besteht der Test.
(V) Refactoring
Zunächst forderte der Compiler, dass ein Zeiger auf bytes.Buffer
übergeben werden muss. Technisch gesehen ist dies korrekt, aber es ist nicht allgemein genug. Um dies zu veranschaulichen, verbinden wir die Funktion Greet
mit einer Go-Anwendung, um Inhalte auf die Standardausgabe zu drucken. Der Code lautet wie folgt:
func main() { Greet(os.Stdout, "Leapcell") }
Beim Ausführen tritt ein Fehler auf:
./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet
Wie bereits erwähnt, ermöglicht fmt.Fprintf
das Übergeben der io.Writer
-Schnittstelle, und sowohl os.Stdout
als auch bytes.Buffer
implementieren diese Schnittstelle. Daher ändern wir den Code, um eine allgemeinere Schnittstelle zu verwenden. Der geänderte Code lautet wie folgt:
package main import ( "fmt" "os" "io" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func main() { Greet(os.Stdout, "Leapcell") }
(VI) Mehr über io.Writer
Durch die Verwendung von io.Writer
wurde die Allgemeinheit unseres Codes verbessert. Beispielsweise können wir Daten ins Internet schreiben. Führen Sie den folgenden Code aus:
package main import ( "fmt" "io" "net/http" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func MyGreeterHandler(w http.ResponseWriter, r *http.Request) { Greet(w, "world") } func main() { http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler)) }
Führen Sie das Programm aus und besuchen Sie http://localhost:5000
, und Sie können sehen, dass die Funktion Greet
aufgerufen wird. Beim Schreiben eines HTTP-Handlers müssen Sie http.ResponseWriter
und http.Request
bereitstellen. http.ResponseWriter
implementiert auch die io.Writer
-Schnittstelle, sodass die Funktion Greet
im Handler wiederverwendet werden kann.
V. Fazit
Die erste Version des Codes ist nicht einfach zu testen, da sie Daten an einen Ort schreibt, der nicht kontrolliert werden kann. Geleitet von den Tests refaktorieren wir den Code. Durch das Injizieren von Abhängigkeiten können wir die Richtung der Datenübertragung steuern, was viele Vorteile mit sich bringt:
- Testen des Codes: Wenn eine Funktion schwer zu testen ist, liegt dies normalerweise daran, dass es feste Verknüpfungen von Abhängigkeiten zur Funktion oder zum globalen Status gibt. Wenn beispielsweise die Dienstschicht einen globalen Datenbankverbindungspool verwendet, ist dies nicht nur schwer zu testen, sondern auch langsam. DI befürwortet das Injizieren der Datenbankabhängigkeit über eine Schnittstelle, um die Mock-Daten im Test zu steuern.
- Trennung von Belangen: Sie entkoppelt den Ort, an dem die Daten ankommen, von der Art und Weise, wie die Daten generiert werden. Wenn Sie das Gefühl haben, dass eine bestimmte Methode/Funktion zu viele Funktionen übernimmt (z. B. das Generieren von Daten und das Schreiben in die Datenbank gleichzeitig oder das Verarbeiten von HTTP-Anforderungen und Geschäftslogik gleichzeitig), müssen Sie möglicherweise das Werkzeug DI verwenden.
- Wiederverwenden von Code in verschiedenen Umgebungen: Der Code wird zuerst in der internen Testumgebung angewendet. Wenn später andere diesen Code verwenden möchten, um neue Funktionen auszuprobieren, müssen sie nur ihre eigenen Abhängigkeiten injizieren.
Leapcell: Die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis
Abschließend möchte ich eine Plattform empfehlen, die sich am besten für die Bereitstellung von Golang eignet: Leapcell
1. Multi-Sprachen-Unterstützung
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
2. Stellen Sie unbegrenzt viele Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen mit einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ