Ich weiß, dass dieser Thread zu diesem Zeitpunkt ziemlich alt ist, aber ich dachte, ich würde mich mit meinen Gedanken dazu einmischen. Das TL; DR ist, dass Sie aufgrund der untypisierten, dynamischen Natur von JavaScript tatsächlich ziemlich viel tun können, ohne auf das DI-Muster (Dependency Injection) zurückzugreifen oder ein DI-Framework zu verwenden. Wenn eine Anwendung jedoch größer und komplexer wird, kann DI definitiv zur Wartbarkeit Ihres Codes beitragen.
DI in C #
Um zu verstehen, warum DI in JavaScript nicht so dringend benötigt wird, ist es hilfreich, eine stark typisierte Sprache wie C # zu betrachten. (Entschuldigung an diejenigen, die C # nicht kennen, aber es sollte leicht zu befolgen sein.) Angenommen, wir haben eine App, die ein Auto und seine Hupe beschreibt. Sie würden zwei Klassen definieren:
class Horn
{
public void Honk()
{
Console.WriteLine("beep!");
}
}
class Car
{
private Horn horn;
public Car()
{
this.horn = new Horn();
}
public void HonkHorn()
{
this.horn.Honk();
}
}
class Program
{
static void Main()
{
var car = new Car();
car.HonkHorn();
}
}
Es gibt nur wenige Probleme beim Schreiben des Codes auf diese Weise.
- Die
Car
Klasse ist eng an die jeweilige Implementierung des Horns in der Horn
Klasse gekoppelt . Wenn wir die Art der vom Auto verwendeten Hupe ändern möchten, müssen wir die Car
Klasse ändern , obwohl sich die Verwendung der Hupe nicht ändert. Dies macht das Testen auch schwierig, da wir die Car
Klasse nicht isoliert von ihrer Abhängigkeit, der Horn
Klasse, testen können .
- Die
Car
Klasse ist für den Lebenszyklus der Horn
Klasse verantwortlich. In einem einfachen Beispiel wie diesem ist dies kein großes Problem, aber in realen Anwendungen haben Abhängigkeiten Abhängigkeiten, Abhängigkeiten usw. Die Car
Klasse müsste für die Erstellung des gesamten Baums ihrer Abhängigkeiten verantwortlich sein. Dies ist nicht nur kompliziert und wiederholt, sondern verstößt auch gegen die "Einzelverantwortung" der Klasse. Es sollte sich darauf konzentrieren, ein Auto zu sein und keine Instanzen zu erstellen.
- Es gibt keine Möglichkeit, dieselben Abhängigkeitsinstanzen wiederzuverwenden. Auch dies ist in dieser Spielzeuganwendung nicht wichtig, aber erwägen Sie eine Datenbankverbindung. Normalerweise haben Sie eine einzelne Instanz, die für Ihre Anwendung freigegeben ist.
Lassen Sie uns dies nun umgestalten, um ein Abhängigkeitsinjektionsmuster zu verwenden.
interface IHorn
{
void Honk();
}
class Horn : IHorn
{
public void Honk()
{
Console.WriteLine("beep!");
}
}
class Car
{
private IHorn horn;
public Car(IHorn horn)
{
this.horn = horn;
}
public void HonkHorn()
{
this.horn.Honk();
}
}
class Program
{
static void Main()
{
var horn = new Horn();
var car = new Car(horn);
car.HonkHorn();
}
}
Wir haben hier zwei wichtige Dinge getan. Zuerst haben wir eine Schnittstelle eingeführt, die unsere Horn
Klasse implementiert. Auf diese Weise können wir die Car
Klasse anstelle der jeweiligen Implementierung für die Schnittstelle codieren . Jetzt könnte der Code alles aufnehmen, was implementiert wird IHorn
. Zweitens haben wir die Horninstanziierung herausgenommen Car
und stattdessen weitergegeben. Dies behebt die oben genannten Probleme und überlässt es der Hauptfunktion der Anwendung, die spezifischen Instanzen und ihre Lebenszyklen zu verwalten.
Dies bedeutet, dass eine neue Art von Hupe für das Auto eingeführt werden könnte, ohne die Car
Klasse zu berühren :
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Der Main könnte FrenchHorn
stattdessen einfach eine Instanz der Klasse einfügen. Dies vereinfacht auch das Testen erheblich. Sie können eine MockHorn
Klasse erstellen , die in den Car
Konstruktor eingefügt werden soll, um sicherzustellen, dass Sie nur die Car
Klasse isoliert testen .
Das obige Beispiel zeigt die manuelle Abhängigkeitsinjektion. Normalerweise wird DI mit einem Framework durchgeführt (z. B. Unity oder Ninject in der C # -Welt ). Diese Frameworks übernehmen die gesamte Abhängigkeitsverdrahtung für Sie, indem sie Ihr Abhängigkeitsdiagramm durchlaufen und nach Bedarf Instanzen erstellen.
Der Standard Node.js Weg
Schauen wir uns nun dasselbe Beispiel in Node.js an. Wir würden unseren Code wahrscheinlich in 3 Module aufteilen:
// horn.js
module.exports = {
honk: function () {
console.log("beep!");
}
};
// car.js
var horn = require("./horn");
module.exports = {
honkHorn: function () {
horn.honk();
}
};
// index.js
var car = require("./car");
car.honkHorn();
Da JavaScript untypisiert ist, haben wir nicht die gleiche enge Kopplung wie zuvor. Schnittstellen sind nicht erforderlich (und existieren auch nicht), da das car
Modul nur versucht, die honk
Methode für alle horn
Exporte des Moduls aufzurufen .
Da Node require
alles zwischenspeichert, sind Module im Wesentlichen Singletons, die in einem Container gespeichert sind. Jede andere Modul , das eine führt require
auf dem horn
Modul genau die gleiche Instanz erhalten. Dies macht das Teilen von Singleton-Objekten wie Datenbankverbindungen sehr einfach.
Jetzt gibt es immer noch das Problem, dass das car
Modul für das Abrufen seiner eigenen Abhängigkeit verantwortlich ist horn
. Wenn Sie möchten, dass das Auto ein anderes Modul für die Hupe verwendet, müssen Sie die require
Anweisung im car
Modul ändern . Dies ist nicht sehr häufig, führt jedoch zu Problemen beim Testen.
Die übliche Art und Weise, wie Benutzer mit dem Testproblem umgehen, ist die Verwendung von Proxyquire . Aufgrund der Dynamik von JavaScript fängt Proxyquire Aufrufe ab, die erforderlich sind, und gibt stattdessen alle von Ihnen bereitgestellten Stubs / Mocks zurück.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Dies ist für die meisten Anwendungen mehr als ausreichend. Wenn es für Ihre App funktioniert, gehen Sie damit. Nach meiner Erfahrung, wenn Anwendungen größer und komplexer werden, wird es jedoch schwieriger, solchen Code zu verwalten.
DI in JavaScript
Node.js ist sehr flexibel. Wenn Sie mit der obigen Methode nicht zufrieden sind, können Sie Ihre Module mithilfe des Abhängigkeitsinjektionsmusters schreiben. In diesem Muster exportiert jedes Modul eine Factory-Funktion (oder einen Klassenkonstruktor).
// horn.js
module.exports = function () {
return {
honk: function () {
console.log("beep!");
}
};
};
// car.js
module.exports = function (horn) {
return {
honkHorn: function () {
horn.honk();
}
};
};
// index.js
var horn = require("./horn")();
var car = require("./car")(horn);
car.honkHorn();
Dies ist sehr ähnlich zu der früheren C # -Methode, da das index.js
Modul beispielsweise für Lebenszyklen und Verkabelung verantwortlich ist. Unit-Tests sind recht einfach, da Sie nur Mocks / Stubs an die Funktionen übergeben können. Auch wenn dies für Ihre Anwendung gut genug ist, machen Sie mit.
Bolus DI Framework
Im Gegensatz zu C # gibt es keine etablierten Standard-DI-Frameworks, die Sie beim Abhängigkeitsmanagement unterstützen. Es gibt eine Reihe von Frameworks in der npm-Registrierung, aber keines ist weit verbreitet. Viele dieser Optionen wurden bereits in den anderen Antworten zitiert.
Ich war mit keiner der verfügbaren Optionen besonders zufrieden und schrieb meinen eigenen Bolus . Bolus wurde entwickelt, um mit Code zu arbeiten, der im obigen DI-Stil geschrieben wurde, und versucht, sehr trocken und sehr einfach zu sein. Mit genau demselben car.js
und den horn.js
oben genannten Modulen können Sie das index.js
Modul mit Bolus wie folgt umschreiben :
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
Die Grundidee ist, dass Sie einen Injektor erstellen. Sie registrieren alle Ihre Module im Injektor. Dann lösen Sie einfach, was Sie brauchen. Bolus geht durch das Abhängigkeitsdiagramm und erstellt und fügt nach Bedarf Abhängigkeiten ein. In einem solchen Spielzeugbeispiel sparen Sie nicht viel, aber in großen Anwendungen mit komplizierten Abhängigkeitsbäumen sind die Einsparungen enorm.
Bolus unterstützt eine Reihe von nützlichen Funktionen wie optionale Abhängigkeiten und Testglobale, aber es gibt zwei Hauptvorteile, die ich im Vergleich zum Standardansatz von Node.js gesehen habe. Wenn Sie viele ähnliche Anwendungen haben, können Sie zunächst ein privates npm-Modul für Ihre Basis erstellen, das einen Injektor erstellt und nützliche Objekte darauf registriert. Dann können Ihre spezifischen Apps nach Bedarf hinzufügen, überschreiben und auflösen, ähnlich wie AngularJSInjektor funktioniert. Zweitens können Sie mit Bolus verschiedene Abhängigkeitskontexte verwalten. Sie können beispielsweise Middleware verwenden, um pro Anforderung einen untergeordneten Injektor zu erstellen, die Benutzer-ID, die Sitzungs-ID, den Logger usw. auf dem Injektor zusammen mit allen davon abhängigen Modulen zu registrieren. Lösen Sie dann, was Sie benötigen, um Anfragen zu bearbeiten. Dies gibt Ihnen Instanzen Ihrer Module pro Anforderung und verhindert, dass Sie den Logger usw. an jeden Modulfunktionsaufruf weitergeben müssen.