Benötige ich eine Abhängigkeitsinjektion in NodeJS oder wie gehe ich damit um?


219

Ich erstelle gerade einige experimentelle Projekte mit nodejs. Ich habe viele Java EE-Webanwendungen mit Spring programmiert und die einfache Möglichkeit der Abhängigkeitsinjektion dort geschätzt.

Jetzt bin ich neugierig: Wie mache ich eine Abhängigkeitsinjektion mit einem Knoten? Oder: Brauche ich es überhaupt? Gibt es ein Ersatzkonzept, weil der Programmierstil anders ist?

Ich spreche bisher über einfache Dinge, wie das Teilen eines Datenbankverbindungsobjekts, aber ich habe keine Lösung gefunden, die mich zufriedenstellt.


1
Wenn Sie sich für DI entscheiden, hat OpenTable kürzlich eine Open-Source-Bibliothek dafür bereitgestellt: github.com/opentable/spur-ioc Ich habe es verwendet (ich arbeite dort) und kann sagen, dass es recht einfach und großartig zum Testen ist.
Tybro0103

Antworten:


107

Kurz gesagt, Sie benötigen keinen Abhängigkeitsinjektionscontainer oder Service-Locater wie in C # / Java. Da Node.js das nutzt module pattern, ist es nicht erforderlich, eine Konstruktor- oder Eigenschaftsinjektion durchzuführen. Obwohl du es immer noch kannst.

Das Tolle an JS ist, dass Sie fast alles ändern können, um das zu erreichen, was Sie wollen. Dies ist praktisch, wenn es um Tests geht.

Siehe mein sehr lahmes erfundenes Beispiel.

MyClass.js::

var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d in dirsOrFiles) {
        if (d === 'error.txt') return true;
    }
    return false;
};

MyClass.test.js::

describe('MyClass', function(){
    it('should return an error if error.txt is found in the directory', function(done){
        var mc = new MyClass();
        assert(mc.errorFileExists('/tmp/mydir')); //true
    });
});

Beachten Sie, wie MyClassabhängig vom fsModul? Wie @ShatyemShekhar erwähnt hat, können Sie tatsächlich Konstruktor- oder Eigenschaftsinjektionen wie in anderen Sprachen durchführen. In Javascript ist dies jedoch nicht erforderlich.

In diesem Fall können Sie zwei Dinge tun.

Sie können die fs.readdirSyncMethode stubben oder beim Aufruf ein völlig anderes Modul zurückgeben require.

Methode 1:

var oldmethod = fs.readdirSync;
fs.readdirSync = function(dir) { 
    return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
};

*** PERFORM TEST ***
*** RESTORE METHOD AFTER TEST ****
fs.readddirSync = oldmethod;

Methode 2:

var oldrequire = require
require = function(module) {
    if (module === 'fs') {
        return {
            readdirSync: function(dir) { 
                return ['somefile.txt', 'error.txt', 'anotherfile.txt']; 
            };
        };
    } else
        return oldrequire(module);

}

Der Schlüssel besteht darin, die Leistung von Node.js und Javascript zu nutzen. Beachten Sie, dass ich ein CoffeeScript-Typ bin, daher ist meine JS-Syntax möglicherweise irgendwo falsch. Ich sage auch nicht, dass dies der beste Weg ist, aber es ist ein Weg. Javascript-Gurus können möglicherweise mit anderen Lösungen mithalten.

Aktualisieren:

Dies sollte Ihre spezifische Frage bezüglich Datenbankverbindungen beantworten. Ich würde ein separates Modul für Sie erstellen, um Ihre Datenbankverbindungslogik zu kapseln. Etwas wie das:

MyDbConnection.js: (Achten Sie darauf, einen besseren Namen zu wählen)

var db = require('whichever_db_vendor_i_use');

module.exports.fetchConnection() = function() {
    //logic to test connection

    //do I want to connection pool?

    //do I need only one connection throughout the lifecyle of my application?

    return db.createConnection(port, host, databasename); //<--- values typically from a config file    
}

Dann würde jedes Modul, das eine Datenbankverbindung benötigt, nur Ihr MyDbConnectionModul enthalten.

SuperCoolWebApp.js::

var dbCon = require('./lib/mydbconnection'); //wherever the file is stored

//now do something with the connection
var connection = dbCon.fetchConnection(); //mydbconnection.js is responsible for pooling, reusing, whatever your app use case is

//come TEST time of SuperCoolWebApp, you can set the require or return whatever you want, or, like I said, use an actual connection to a TEST database. 

Folgen Sie diesem Beispiel nicht wörtlich. Es ist ein lahmes Beispiel für den Versuch zu kommunizieren, dass Sie das moduleMuster nutzen, um Ihre Abhängigkeiten zu verwalten. Hoffentlich hilft das ein bisschen mehr.


42
Dies gilt in Bezug auf Tests, aber DI hat andere Vorteile; Mit DI können Sie auf eine Schnittstelle programmieren, nicht auf eine Implementierung.
Moteutsch

3
@moteutsch Ich bin mir nicht sicher, warum Sie das tun würden, da JS nicht wie die meisten statischen Sprachen über Schnittstellen verfügt. Alles, was Sie wirklich haben, sind Implementierungen, auch wenn Sie eine vorher vereinbarte dokumentierte "Schnittstelle" verwenden wollten.
JP Richardson

16
@JPRichardson Wie kann ich eine Komponente schreiben, die einen Logger verwendet, ohne von einer Bibliothek abhängig zu sein? Wenn ich require('my_logger_library'), müssen Benutzer, die meine Komponente verwenden, die Anforderung überschreiben, ihre eigene Bibliothek zu verwenden. Stattdessen kann ich zulassen, dass Benutzer einen Rückruf übergeben, der eine Logger-Implementierung in die Komponentenmethode "Konstruktor" oder "Init" einschließt. Das ist der Zweck von DI.
Moteutsch

4
Ab Mitte 2014 - npmjs.org/package/proxyquire macht das Verspotten von "erforderlichen" Abhängigkeiten trivial.
Arcseldon

4
Ich verstehe es nicht, das Ersetzen von require in einem Modul ersetzt es nicht in einem anderen. Wenn ich in meinem Test require auf eine Funktion setze und dann das zu testende Modul anfordern möchte, verwenden die require-Anweisungen im zu testenden Objekt nicht den im Testmodul festgelegten Funktionssatz. Wie werden dadurch Abhängigkeiten injiziert?
HMR

72

requireist die Art und Weise, Abhängigkeiten in Node.js zu verwalten, und sicherlich intuitiv und effektiv, hat aber auch seine Grenzen.

Mein Rat ist, einen Blick auf einige der heute für Node.js verfügbaren Dependency Injection-Container zu werfen, um eine Vorstellung von ihren Vor- und Nachteilen zu bekommen. Einige von ihnen sind:

Nur um ein paar zu nennen.

Die eigentliche Frage ist nun, was Sie mit einem Node.js DI-Container im Vergleich zu einem einfachen erreichen können require.

Vorteile:

  • Bessere Testbarkeit: Module akzeptieren ihre Abhängigkeiten als Eingabe
  • Umkehrung der Steuerung: Entscheiden Sie, wie Sie Ihre Module verkabeln möchten, ohne den Hauptcode Ihrer Anwendung zu berühren.
  • Ein anpassbarer Algorithmus zum Auflösen von Modulen: Abhängigkeiten haben "virtuelle" Bezeichner, normalerweise sind sie nicht an einen Pfad im Dateisystem gebunden.
  • Bessere Erweiterbarkeit: Aktiviert durch IoC und "virtuelle" Bezeichner.
  • Andere ausgefallene Sachen möglich:
    • Asynchrone Initialisierung
    • Modullebenszyklusmanagement
    • Erweiterbarkeit des DI-Containers
    • Kann leicht Abstraktionen auf höherer Ebene implementieren (z. B. AOP)

Nachteile:

  • Anders als bei der "Erfahrung" von Node.j: requireWenn Sie nicht verwenden, fühlen Sie sich definitiv so, als würden Sie von der Node.-Denkweise abweichen.
  • Die Beziehung zwischen einer Abhängigkeit und ihrer Implementierung ist nicht immer explizit. Eine Abhängigkeit kann zur Laufzeit aufgelöst und durch verschiedene Parameter beeinflusst werden. Der Code wird schwieriger zu verstehen und zu debuggen
  • Langsamere Startzeit
  • Reife (im Moment): Keine der aktuellen Lösungen ist im Moment wirklich beliebt, daher nicht so viele Tutorials, kein Ökosystem, nicht kampferprobt.
  • Einige DI-Container funktionieren nicht gut mit Modulbündlern wie Browserify und Webpack.

Wie bei allem, was mit Softwareentwicklung zu tun hat, requirehängt die Wahl zwischen DI oder von Ihren Anforderungen, Ihrer Systemkomplexität und Ihrem Programmierstil ab.


3
Denken Sie, dass sich die Situation seit '09 erheblich geändert hat?
Juho Vepsäläinen

13
Meinst du seit 10 Tagen? :)
Mario

2
Nein. 9. Dezember ... hätte es wissen müssen.
Juho Vepsäläinen

4
Ich habe DI mit dem Muster "module.exports = function (deps) {}" implementiert ". Ja, es funktioniert, aber es ist nicht ganz ideal.
Juho Vepsäläinen

3
Module akzeptieren ihre Abhängigkeiten als Eingabe und Abhängigkeiten sind für mich keine expliziten Klänge wie ein Widerspruch.
Anton Rudeshko

53

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.

  1. Die CarKlasse ist eng an die jeweilige Implementierung des Horns in der HornKlasse gekoppelt . Wenn wir die Art der vom Auto verwendeten Hupe ändern möchten, müssen wir die CarKlasse ändern , obwohl sich die Verwendung der Hupe nicht ändert. Dies macht das Testen auch schwierig, da wir die CarKlasse nicht isoliert von ihrer Abhängigkeit, der HornKlasse, testen können .
  2. Die CarKlasse ist für den Lebenszyklus der HornKlasse 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 CarKlasse 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.
  3. 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 HornKlasse implementiert. Auf diese Weise können wir die CarKlasse 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 Carund 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 CarKlasse zu berühren :

class FrenchHorn : IHorn
{
    public void Honk()
    {
        Console.WriteLine("le beep!");
    }
}

Der Main könnte FrenchHornstattdessen einfach eine Instanz der Klasse einfügen. Dies vereinfacht auch das Testen erheblich. Sie können eine MockHornKlasse erstellen , die in den CarKonstruktor eingefügt werden soll, um sicherzustellen, dass Sie nur die CarKlasse 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 carModul nur versucht, die honkMethode für alle hornExporte des Moduls aufzurufen .

Da Node requirealles zwischenspeichert, sind Module im Wesentlichen Singletons, die in einem Container gespeichert sind. Jede andere Modul , das eine führt requireauf dem hornModul 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 carModul 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 requireAnweisung im carModul ä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.jsModul 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.jsund den horn.jsoben genannten Modulen können Sie das index.jsModul 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.


1
Es ist auch wahr, dass es Alternativen zu proxyquiresolchen gibt, sinondie es Ihnen ermöglichen, sehr präzise Verspottungen durchzuführen, z. B. let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));und nachfolgende Aufrufe von fs.readFilegeben Fehler zurück, bis Sie den Stub über zurücksetzen readFileStub.restore(). Persönlich finde ich DI von fragwürdigem Nutzen, weil ich das Gefühl habe, dass es fast die Verwendung von Klassen / Objekten erfordert, was angesichts der funktionalen Neigungen von Javascript eine zweifelhafte Annahme ist.
Kevin

Vielen Dank für diese gute + detaillierte Antwort. Ich habe es fast verpasst, als ich zuerst die Überschrift DI in C # las .
Konstantin A. Magg

1
Gute Antwort. Ich frage mich , was Ihre Gedanken in 2019. Für große Projekte sind in der Frage der persönlichen Vorliebe, was bevorzugen Sie - DI / IoC in Knoten, oder einfach nur stubbing / spöttisch mit jest, rewire, proxyquireetc.? Vielen Dank.
Jamie Corkhill

Tolle ausgewogene Antwort! Danke dir.
Johnny Oshika

36

Ich habe auch ein Modul geschrieben, um dies zu erreichen. Es heißt rewire . Einfach benutzen npm install rewireund dann:

var rewire = require("rewire"),
    myModule = rewire("./path/to/myModule.js"); // exactly like require()

// Your module will now export a special setter and getter for private variables.
myModule.__set__("myPrivateVar", 123);
myModule.__get__("myPrivateVar"); // = 123


// This allows you to mock almost everything within the module e.g. the fs-module.
// Just pass the variable name as first parameter and your mock as second.
myModule.__set__("fs", {
    readFile: function (path, encoding, cb) {
        cb(null, "Success!");
    }
});
myModule.readSomethingFromFileSystem(function (err, data) {
    console.log(data); // = Success!
});

Ich habe mich von Nathan MacInnes 'Injektion inspirieren lassen, aber einen anderen Ansatz gewählt. Ich benutze nicht, vmum das Testmodul zu evaluieren, sondern ich benutze die eigenen Anforderungen des Knotens. Auf diese Weise verhält sich Ihr Modul genau wie die Verwendung require()(mit Ausnahme Ihrer Änderungen). Auch das Debuggen wird vollständig unterstützt.


7
Ab Mitte 2014 - npmjs.org/package/proxyquire macht das Verspotten von "erforderlichen" Abhängigkeiten trivial.
Arcseldon

Proxyquire ist auch cool! Sie verwenden jetzt das interne "Modul" -Modul, was viel besser ist als die Verwendung der VM des Knotens. Aber es ist doch nur eine Frage des Stils. Ich mag es, wenn mein Modul die ursprüngliche Anforderung verwendet und die Abhängigkeiten später austauscht. Darüber hinaus ermöglicht das Neuverdrahten auch das Überschreiben von Globals.
Johannes Ewald

Sehr interessant war die Suche nach so etwas für die Arbeit. Betrifft dieses Modul auch nachgeschaltete Module?
Akst

for proxyquire In der Beschreibung heißt es, dass es zum Testen verwendet wird: "Proxies nodejs erfordern, um das Überschreiben von Abhängigkeiten während des Testens zu ermöglichen ." nicht für DI, richtig?
Marwen Trabelsi

17

Ich habe Elektrolyt nur für diesen Zweck gebaut. Die anderen Abhängigkeitsinjektionslösungen da draußen waren für meinen Geschmack zu invasiv, und das Durcheinander mit dem Globalen requireist eine besondere Beschwerde von mir.

Elektrolyt umfasst Module, insbesondere solche, die eine "Setup" -Funktion exportieren, wie Sie sie in der Connect / Express-Middleware sehen. Im Wesentlichen sind diese Modultypen nur Fabriken für ein Objekt, das sie zurückgeben.

Beispiel: Ein Modul, das eine Datenbankverbindung erstellt:

var mysql = require('mysql');

exports = module.exports = function(settings) {
  var connection = mysql.createConnection({
    host: settings.dbHost,
    port: settings.dbPort
  });

  connection.connect(function(err) {
    if (err) { throw err; }
  });

  return connection;
}

exports['@singleton'] = true;
exports['@require'] = [ 'settings' ];

Was Sie unten sehen, sind Anmerkungen , ein zusätzliches Stück Metadaten, das Electrolyte verwendet, um Abhängigkeiten zu instanziieren und einzufügen, wobei die Komponenten Ihrer Anwendung automatisch miteinander verbunden werden.

So erstellen Sie eine Datenbankverbindung:

var db = electrolyte.create('database');

Der Elektrolyt durchläuft transitiv die @require'd-Abhängigkeiten und fügt Instanzen als Argumente in die exportierte Funktion ein.

Der Schlüssel ist, dass dies minimal invasiv ist. Dieses Modul ist unabhängig vom Elektrolyten selbst vollständig verwendbar. Das bedeutet, dass Ihre Komponententests nur das zu testende Modul testen können und Scheinobjekte übergeben, ohne dass zusätzliche Abhängigkeiten erforderlich sind, um Interna neu zu verdrahten.

Wenn die vollständige Anwendung ausgeführt wird, greift Electrolyte auf der Ebene zwischen den Modulen ein und verdrahtet die Dinge, ohne dass Globals, Singletons oder übermäßige Leitungen erforderlich sind.


1
Würden Sie klarstellen, was in dem Code passiert, den Sie bei einem Aufruf zum connect()Werfen gepostet haben ? Obwohl ich mit der MySql-API für Node nicht vertraut bin, würde ich erwarten, dass dieser Aufruf asynchron ist, sodass die Abbildung nicht ganz klar ist.
Andrey Agibalov

Derzeit wird Elektrolyt verwendet. Sie behaupten, dass es einfach ist, Module über Exporte ['@ require'] zu INJEKTIEREN. Aber wenn ich eines der erforderlichen Module stummschalten muss, wie ist das im Elektrolyten erreichbar? Derzeit, wenn wir Module benötigen, kann dies leicht erreicht werden. Aber für Elektrolyt ist dies ein großer Nachteil ... Haben Sie Beispiele, bei denen wir gestoppelte Versionen von Modulen verwenden und diese dynamisch übergeben können, während Instanziierung / ioc.use aus den Testfällen verwendet wird. Im Grunde genommen wäre es im Unit-Test ideal, wenn wir ioc.create ('Modulname') und dann die Injektion von abhängigen Modulen (aber gestoppelten) durchführen
könnten

1
Sie würden nicht ioc.createvon einem Unit-Test anrufen . Ein Komponententest sollte nur das zu testende Modul testen und keine anderen Abhängigkeiten, einschließlich Elektrolyt, berücksichtigen. Wenn Sie diesem Rat folgen, würden Sie tunobjToTest = require('modulename')(mockObj1, mockObj2);
Jared Hanson

8

Ich habe das selbst untersucht. Ich mag es nicht, magische Abhängigkeits-Utils-Bibliotheken einzuführen, die Mechanismen zum Entführen meiner Modulimporte bereitstellen. Stattdessen habe ich eine "Konstruktionsrichtlinie" für mein Team erstellt, in der ziemlich explizit angegeben wird, welche Abhängigkeiten verspottet werden können, indem ein Factory-Funktionsexport in meine Module eingeführt wird.

Ich verwende ES6-Funktionen in großem Umfang für Parameter und Destrukturierung, um einige Boilerplates zu vermeiden und einen benannten Mechanismus zum Überschreiben von Abhängigkeiten bereitzustellen.

Hier ist ein Beispiel:

import foo from './utils/foo';
import bob from './utils/bob';

// We export a factory which accepts our dependencies.
export const factory = (dependencies = {}) => {
  const {
    // The 'bob' dependency.  We default to the standard 'bob' imp if not provided.
    $bob = bob, 
    // Instead of exposing the whole 'foo' api, we only provide a mechanism
    // with which to override the specific part of foo we care about.
    $doSomething = foo.doSomething // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $bob($doSomething());
  }
}

// The default implementation, which would end up using default deps.
export default factory();

Und hier ist ein Beispiel für die Verwendung

import { factory } from './bar';

const underTest = factory({ $bob: () => 'BOB!' }); // only override bob!
const result = underTest();

Entschuldigen Sie die ES6-Syntax für diejenigen, die damit nicht vertraut sind.


Sehr genial!
Arnold

4

Ich habe diesen Thread kürzlich aus dem gleichen Grund wie das OP überprüft - die meisten Bibliotheken, auf die ich gestoßen bin, haben die require-Anweisung vorübergehend neu geschrieben. Ich hatte gemischte Erfolge mit dieser Methode und habe daher den folgenden Ansatz gewählt.

Im Kontext einer Express-Anwendung - ich verpacke app.js in eine bootstrap.js-Datei:

var path = require('path');
var myapp = require('./app.js');

var loader = require('./server/services/loader.js');

// give the loader the root directory
// and an object mapping module names 
// to paths relative to that root
loader.init(path.normalize(__dirname), require('./server/config/loader.js')); 

myapp.start();

Die an den Loader übergebene Objektzuordnung sieht folgendermaßen aus:

// live loader config
module.exports = {
    'dataBaseService': '/lib/dataBaseService.js'
}

// test loader config
module.exports = {
    'dataBaseService': '/mocks/dataBaseService.js'
    'otherService' : {other: 'service'} // takes objects too...
};

Dann, anstatt direkt anzurufen, benötigen ...

var myDatabaseService = loader.load('dataBaseService');

Wenn sich im Loader kein Alias ​​befindet, wird standardmäßig eine reguläre Anforderung verwendet. Dies hat zwei Vorteile: Ich kann jede Version der Klasse austauschen, sodass keine relativen Pfadnamen in der gesamten Anwendung verwendet werden müssen. Wenn ich also eine benutzerdefinierte Bibliothek unter oder über der aktuellen Datei benötige, muss ich nicht durchlaufen , und require wird das Modul gegen denselben Schlüssel zwischenspeichern). Außerdem kann ich Mocks an jedem Punkt in der App und nicht in der unmittelbaren Testsuite angeben.

Ich habe gerade ein kleines npm-Modul veröffentlicht:

https://npmjs.org/package/nodejs-simple-loader


3

Die Realität ist, dass Sie Ihre node.js ohne IoC-Container testen können, da JavaScript eine sehr dynamische Programmiersprache ist und Sie fast alles zur Laufzeit ändern können.

Folgendes berücksichtigen:

import UserRepository from "./dal/user_repository";

class UserController {
    constructor() {
        this._repository = new UserRepository();
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

So können Sie die Kopplung zwischen Komponenten zur Laufzeit überschreiben. Ich denke gerne, dass wir versuchen sollten, unsere JavaScript-Module zu entkoppeln.

Die einzige Möglichkeit, eine echte Entkopplung zu erreichen, besteht darin, den Verweis auf Folgendes zu entfernen UserRepository:

class UserController {
    constructor(userRepository) {
        this._repository = userRepository;
    }
    getUsers() {
        this._repository.getAll();
    }
}

export default UserController;

Dies bedeutet, dass Sie irgendwo anders die Objektkomposition durchführen müssen:

import UserRepository from "./dal/user_repository";
import UserController from "./dal/user_controller";

export default new UserController(new UserRepository());

Ich mag die Idee, die Objektzusammensetzung an einen IoC-Container zu delegieren. Weitere Informationen zu dieser Idee finden Sie im Artikel Der aktuelle Status der Abhängigkeitsinversion in JavaScript . Der Artikel versucht, einige "JavaScript IoC Container Mythen" zu entlarven:

Mythos 1: In JavaScript ist kein Platz für IoC-Container

Mythos 2: Wir brauchen keine IoC-Container, wir haben bereits Modullader!

Mythos 3: Abhängigkeitsinversion === Abhängigkeiten injizieren

Wenn Ihnen auch die Idee gefällt, einen IoC-Container zu verwenden, können Sie sich InversifyJS ansehen. Die neueste Version (2.0.0) unterstützt viele Anwendungsfälle:

  • Kernelmodule
  • Kernel-Middleware
  • Verwenden Sie Klassen, Zeichenfolgenliterale oder Symbole als Abhängigkeitskennungen
  • Injektion konstanter Werte
  • Injektion von Klassenkonstruktoren
  • Injektion von Fabriken
  • Autofabrik
  • Injektion von Anbietern (asynchrone Fabrik)
  • Aktivierungshandler (zum Injizieren von Proxys)
  • Mehrfachinjektionen
  • Markierte Bindungen
  • Benutzerdefinierte Tag Dekorateure
  • Benannte Bindungen
  • Kontextbindungen
  • Freundliche Ausnahmen (zB kreisförmige Abhängigkeiten)

Weitere Informationen finden Sie unter InversifyJS .


2

Für ES6 habe ich diesen Container https://github.com/zazoomauro/node-dependency-injection entwickelt

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container.register('mailer', 'Mailer')

Dann können Sie zum Beispiel die Wahl des Transports im Container einstellen:

import {ContainerBuilder} from 'node-dependency-injection'

let container = new ContainerBuilder()
container
  .register('mailer', 'Mailer')
  .addArgument('sendmail')

Diese Klasse ist jetzt viel flexibler, da Sie die Auswahl des Transports von der Implementierung in den Container getrennt haben.

Nachdem sich der Mailer-Dienst im Container befindet, können Sie ihn als Abhängigkeit von anderen Klassen einfügen. Wenn Sie eine NewsletterManager-Klasse wie diese haben:

class NewsletterManager {
    construct (mailer, fs) {
        this._mailer = mailer
        this._fs = fs
    }
}

export default NewsletterManager

Bei der Definition des Dienstes newsletter_manager ist der Mailer-Dienst noch nicht vorhanden. Verwenden Sie die Referenzklasse, um den Container anzuweisen, den Mailer-Service zu injizieren, wenn er den Newsletter-Manager initialisiert:

import {ContainerBuilder, Reference, PackageReference} from 'node-dependency-injection'
import Mailer from './Mailer'
import NewsletterManager from './NewsletterManager'

let container = new ContainerBuilder()

container
  .register('mailer', Mailer)
  .addArgument('sendmail')

container
  .register('newsletter_manager', NewsletterManager)
  .addArgument(new Reference('mailer'))
  .addArgument(new PackageReference('fs-extra'))

Sie können den Container auch mit Konfigurationsdateien wie Yaml-, Json- oder JS-Dateien einrichten

Der Service-Container kann aus verschiedenen Gründen zusammengestellt werden. Zu diesen Gründen gehört es, nach potenziellen Problemen wie Zirkelverweisen zu suchen und den Container effizienter zu gestalten.

container.compile()

1

Dies hängt vom Design Ihrer Anwendung ab. Sie können natürlich eine Java-ähnliche Injektion ausführen, bei der Sie ein Objekt einer Klasse mit der im Konstruktor übergebenen Abhängigkeit erstellen.

function Cache(store) {
   this._store = store;
}

var cache = new Cache(mysqlStore);

Wenn Sie OOP nicht in Javascript ausführen, können Sie eine Init-Funktion erstellen, die alles einrichtet.

Es gibt jedoch einen anderen Ansatz, der in einem ereignisbasierten System wie node.js üblicher ist. Wenn Sie Ihre Anwendung so modellieren können, dass sie nur (meistens) auf Ereignisse reagiert, müssen Sie lediglich alles einrichten (was ich normalerweise durch Aufrufen einer Init-Funktion mache) und Ereignisse von einem Stub ausgeben. Dies macht das Testen ziemlich einfacher und lesbarer.


Vielen Dank für Ihre Antwort, aber ich verstehe Ihren zweiten Teil Ihrer Antwort nicht vollständig.
Erik

1

Die Einfachheit des IoC-Konzepts hat mir immer gefallen: "Sie müssen nichts über die Umgebung wissen, Sie werden bei Bedarf von jemandem angerufen."

Aber alle IoC-Implementierungen, die ich gesehen habe, haben genau das Gegenteil bewirkt - sie überladen den Code mit noch mehr Dingen als ohne. Also habe ich mein eigenes IoC erstellt, das so funktioniert, wie ich es gerne hätte - es bleibt 90% der Zeit verborgen und unsichtbar .

Es wird im MonoJS-Webframework http://monojs.org verwendet

Ich spreche bisher über einfache Dinge, wie das Teilen eines Datenbankverbindungsobjekts, aber ich habe keine Lösung gefunden, die mich zufriedenstellt.

Es geht so - registrieren Sie die Komponente einmal in der Konfiguration.

app.register 'db', -> 
  require('mongodb').connect config.dbPath

Und überall verwenden

app.db.findSomething()

Den vollständigen Komponentendefinitionscode (mit DB Connection und anderen Komponenten) finden Sie hier https://github.com/sinizinairina/mono/blob/master/mono.coffee

Dies ist der einzige Ort, an dem Sie IoC mitteilen müssen, was zu tun ist. Danach werden alle diese Komponenten automatisch erstellt und verkabelt, und Sie müssen keinen IoC-spezifischen Code mehr in Ihrer Anwendung sehen.

Das IoC selbst https://github.com/alexeypetrushin/miconjs


6
Obwohl als DI beworben, scheint dies viel mehr wie ein Service Locator zu sein.
KyorCode

2
Sieht gut aus, schade, dass es nur in Coffescript ist
Rafael P. Miranda

1

Ich denke, wir brauchen immer noch Dependency Injection in Nodejs, weil es die Abhängigkeiten zwischen Diensten löst und die Anwendung klarer macht.

Inspiriert von Spring Framework implementiere ich auch mein eigenes Modul, um die Abhängigkeitsinjektion in Nodejs zu unterstützen. Mein Modul kann auch die code changesund auto reloaddie Dienste erkennen, ohne Ihre Anwendung neu zu starten.

Besuchen Sie mein Projekt unter: Buncha - IoC Container

Danke dir!



0

Ich habe lange mit .Net, PHP und Java gearbeitet, daher wollte ich auch in NodeJS eine bequeme Abhängigkeitsinjektion haben. Die Leute sagten, dass die in NodeJS integrierte DI ausreicht, da wir sie mit Module erhalten können. Aber es hat mich nicht gut befriedigt. Ich wollte ein Modul nicht mehr als eine Klasse behalten. Außerdem wollte ich, dass der DI die Verwaltung des Modullebenszyklus (Singleton-Modul, Transient-Modul usw.) vollständig unterstützt, aber mit dem Node-Modul musste ich sehr oft manuellen Code schreiben. Zuletzt wollte ich Unit Test einfacher machen. Deshalb habe ich mir eine Abhängigkeitsinjektion erstellt.

Wenn Sie nach einem DI suchen, probieren Sie es aus. Es kann hier gefunden werden: https://github.com/robo-creative/nodejs-robo-container . Es ist vollständig dokumentiert. Es werden auch einige häufig auftretende Probleme mit DI und deren Lösung auf OOP-Weise behandelt. Ich hoffe es hilft.


Ja, Sie haben Recht, eine DI-Bibliothek in Ihren Projekten ist wichtig für gute Architekturen. Wenn Sie einen Anwendungsfall für DI sehen möchten, lesen Sie in der Readme-Datei für dieses Repository auch eine DI-Bibliothek für den Knoten Jems DI .
Francisco Mercedes

-1

Ich habe kürzlich eine Bibliothek namens Circuitbox erstellt , mit der Sie die Abhängigkeitsinjektion mit node.js verwenden können. Es macht echte Abhängigkeitsinjektion im Vergleich zu vielen der auf Abhängigkeitssuche basierenden Bibliotheken, die ich gesehen habe. Circuitbox unterstützt auch Routinen zur asynchronen Erstellung und Initialisierung. Unten ist ein Beispiel:

Angenommen, der folgende Code befindet sich in einer Datei namens consoleMessagePrinter.js

'use strict';

// Our console message printer
// deps is injected by circuitbox with the dependencies
function ConsoleMessagePrinter(deps) {
  return {
    print: function () {
      console.log(deps.messageSource.message());
    }
  };
}

module.exports = ConsoleMessagePrinter;

Angenommen, in der Datei main.js befindet sich Folgendes

'use strict';

// our simple message source
// deps is injected by circuitbox with the dependencies
var simpleMessageSource = function (deps) {
  return {
    message: function () {
      return deps.message;
    }
  };
};

// require circuitbox
var circuitbox = require('../lib');

// create a circuitbox
circuitbox.create({
  modules: [
    function (registry) {
      // the message to be used
      registry.for('message').use('This is the message');

      // define the message source
      registry.for('messageSource').use(simpleMessageSource)
        .dependsOn('message');

      // define the message printer - does a module.require internally
      registry.for('messagePrinter').requires('./consoleMessagePrinter')
        .dependsOn('messageSource');
    }
  ]
}).done(function (cbx) {

  // get the message printer and print a message
  cbx.get('messagePrinter').done(function (printer) {
    printer.print();
  }, function (err) {
    console.log('Could not recieve a printer');
    return;
  });

}, function (err) {
  console.log('Could not create circuitbox');
});

Mit Circuitbox können Sie Ihre Komponenten definieren und ihre Abhängigkeiten als Module deklarieren. Nach der Initialisierung können Sie eine Komponente abrufen. Circuitbox injiziert automatisch alle Komponenten, die die Zielkomponente benötigt, und gibt sie Ihnen zur Verwendung.

Das Projekt ist in der Alpha-Version. Ihre Kommentare, Ideen und Rückmeldungen sind willkommen.

Ich hoffe es hilft!


-1

Ich denke, andere Beiträge haben bei der Argumentation für die Verwendung von DI großartige Arbeit geleistet. Für mich sind die Gründe

  1. Injizieren Sie Abhängigkeiten, ohne ihre Pfade zu kennen. Dies bedeutet, dass Sie nicht jede davon abhängige Datei berühren müssen, wenn Sie einen Modulspeicherort auf der Festplatte ändern oder durch einen anderen austauschen.

  2. Es macht es viel einfacher, Abhängigkeiten zum Testen zu verspotten, ohne die globale requireFunktion auf eine Weise überschreiben zu müssen, die problemlos funktioniert.

  3. Es hilft Ihnen, Ihre Anwendung als lose gekoppelte Module zu organisieren und zu begründen.

Es fiel mir jedoch sehr schwer, ein DI-Framework zu finden, das mein Team und ich problemlos übernehmen können. Deshalb habe ich kürzlich ein Framework namens deppie erstellt, das auf diesen Funktionen basiert

  • Minimale API, die in wenigen Minuten erlernt werden kann
  • Kein zusätzlicher Code / Konfiguration / Anmerkungen erforderlich
  • Eins zu eins direkte Zuordnung zu require Modulen
  • Kann teilweise übernommen werden, um mit vorhandenem Code zu arbeiten


-1

Node.js benötigt DI genauso wie jede andere Plattform. Wenn Sie etwas Großes erstellen, wird DI es einfacher machen, die Abhängigkeiten Ihres Codes zu verspotten und Ihren Code gründlich zu testen.

Beispielsweise sollten Ihre Datenbankschichtmodule nicht nur für Ihre Geschäftscodemodule erforderlich sein, da beim Testen dieser Geschäftscodemodule durch die Einheit die Daos geladen werden und eine Verbindung zur Datenbank hergestellt wird.

Eine Lösung wäre, die Abhängigkeiten als Modulparameter zu übergeben:

module.exports = function (dep1, dep2) {
// private methods

   return {
    // public methods
       test: function(){...}
   }
}

Auf diese Weise können Abhängigkeiten einfach und natürlich verspottet werden und Sie können sich weiterhin auf das Testen Ihres Codes konzentrieren, ohne eine knifflige Bibliothek von Drittanbietern zu verwenden.

Es gibt andere Lösungen (Broadway, Architekt usw.), die Ihnen dabei helfen können. obwohl sie mehr tun können, als Sie wollen, oder mehr Unordnung verwenden.


Fast durch natürliche Evolution habe ich das Gleiche getan. Ich übergebe eine Abhängigkeit als Parameter und sie funktioniert hervorragend zum Testen.
Munkee

-1

Ich habe eine Bibliothek entwickelt, die die Abhängigkeitsinjektion auf einfache Weise handhabt und den Boilerplate-Code verringert. Jedes Modul wird durch einen eindeutigen Namen und eine Steuerungsfunktion definiert. Die Parameter der Steuerung spiegeln die Abhängigkeiten des Moduls wider.

Lesen Sie mehr über KlarkJS

Kurzes Beispiel:

KlarkModule(module, 'myModuleName1', function($nodeModule1, myModuleName2) {
    return {
        log: function() { console.log('Hello from module myModuleName1') }
    };
});
  • myModuleName1 ist der Name des Moduls.
  • $nodeModule1ist eine externe Bibliothek von node_module. Der Name wird in aufgelöst node-module1. Das Präfix $gibt an, dass es sich um ein externes Modul handelt.
  • myModuleName2 ist der Name eines internen Moduls.
  • Der Rückgabewert der Steuerung wird von den anderen internen Modulen verwendet, wenn diese den Parameter definieren myModuleName1.

-1

Ich habe diese Frage bei der Beantwortung eines Problems entdeckt in meinem eigenen DI-Modul geantwortet habe, warum man jemals ein DI-System für die NodeJS-Programmierung benötigen würde.

Die Antwort tendierte eindeutig zu den in diesem Thread angegebenen: Es kommt darauf an. Es gibt Kompromisse für beide Ansätze, und das Lesen der Antworten dieser Frage gibt ihnen eine gute Form.

Die eigentliche Antwort auf diese Frage sollte also sein, dass Sie in einigen Situationen ein DI-System verwenden würden, in anderen nicht.

Als Entwickler möchten Sie sich jedoch nicht wiederholen und Ihre Dienste in Ihren verschiedenen Anwendungen wiederverwenden.

Dies bedeutet, dass wir Dienste schreiben sollten, die zur Verwendung im DI-System bereit sind, aber nicht an DI-Bibliotheken gebunden sind. Für mich bedeutet das, dass wir Dienste wie diesen schreiben sollten:

module.exports = initDBService;

// Tells any DI lib what it expects to find in it context object
// The $inject prop is the de facto standard for DI imo 
initDBService.$inject = ['ENV'];

// Note the context object, imo, a DI tool should bring
// services in a single context object
function initDBService({ ENV }) {
/// actual service code
}

Auf diese Weise funktioniert Ihr Dienst unabhängig davon, ob Sie ihn mit oder ohne DI-Tool verwenden.


-1

TypeDI ist das süßeste von allen hier genannten, schauen Sie sich diesen Code in TypeDI an

import "reflect-metadata";
import {Service, Container} from "typedi";

@Service()
class SomeClass {

    someMethod() {
    }

}

let someClass = Container.get(SomeClass);
someClass.someMethod();

Schauen Sie sich auch diesen Code an:

import {Container, Service, Inject} from "typedi";

// somewhere in your global app parameters
Container.set("authorization-token", "RVT9rVjSVN");

@Service()
class UserRepository {

    @Inject("authorization-token")
    authorizationToken: string;

}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.