Ai-je besoin d'une injection de dépendance dans NodeJS, ou comment gérer…?

219

Je crée actuellement des projets expérimentaux avec nodejs. J'ai programmé beaucoup d'applications Web Java EE avec Spring et apprécié la facilité d'injection de dépendances là-bas.

Maintenant, je suis curieux: comment puis-je faire l'injection de dépendance avec le nœud? Ou: en ai-je même besoin? Y a-t-il un concept de remplacement, car le style de programmation est différent?

Je parle de choses simples, comme partager un objet de connexion à une base de données, jusqu'à présent, mais je n'ai pas trouvé de solution qui me satisfasse.

Erik
la source
1
Si vous décidez d'utiliser DI, OpenTable a récemment ouvert une bibliothèque open source: github.com/opentable/spur-ioc Je l'ai utilisé (j'y travaille) et je peux dire que c'est assez simple et idéal pour les tests.
tybro0103

Réponses:

107

En bref, vous n'avez pas besoin d'un conteneur d'injection de dépendances ou d'un localisateur de services comme vous le feriez en C # / Java. Depuis Node.js, exploite le module pattern, il n'est pas nécessaire d'effectuer une injection de constructeur ou de propriété. Bien que vous puissiez toujours.

La grande chose à propos de JS est que vous pouvez modifier à peu près n'importe quoi pour obtenir ce que vous voulez. Cela est très utile pour les tests.

Voici mon exemple artificiel très boiteux.

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
    });
});

Remarquez comment cela MyClassdépend du fsmodule? Comme l'a mentionné @ShatyemShekhar, vous pouvez en effet faire une injection de constructeur ou de propriété comme dans d'autres langues. Mais ce n'est pas nécessaire en Javascript.

Dans ce cas, vous pouvez faire deux choses.

Vous pouvez bloquer la fs.readdirSyncméthode ou renvoyer un module entièrement différent lorsque vous appelez require.

Méthode 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;

Méthode 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);

}

La clé est de tirer parti de la puissance de Node.js et Javascript. Remarque, je suis un gars CoffeeScript, donc ma syntaxe JS peut être incorrecte quelque part. De plus, je ne dis pas que c'est la meilleure façon, mais c'est une façon. Les gourous de Javascript pourraient être en mesure d'intégrer d'autres solutions.

Mettre à jour:

Cela devrait répondre à votre question spécifique concernant les connexions à la base de données. Je créerais un module distinct pour que vous encapsuliez votre logique de connexion à la base de données. Quelque chose comme ça:

MyDbConnection.js: (assurez-vous de choisir un meilleur nom)

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    
}

Ensuite, tout module qui a besoin d'une connexion à une base de données inclurait alors simplement votre MyDbConnectionmodule.

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. 

Ne suivez pas cet exemple textuellement. C'est un exemple boiteux d'essayer de communiquer que vous exploitez le modulemodèle pour gérer vos dépendances. Espérons que cela aide un peu plus.

JP Richardson
la source
42
Cela est vrai en ce qui concerne les tests, mais DI a d'autres avantages; en utilisant DI, vous pouvez programmer vers une interface, pas une implémentation.
moteutsch
3
@moteutsch Je ne sais pas pourquoi vous le feriez puisque JS n'a pas la notion d'interfaces comme la plupart des langages statiques. Tout ce que vous avez vraiment, ce sont des implémentations, même si vous voulez utiliser une "interface" documentée et convenue au préalable.
JP Richardson
16
@JPRichardson Comment puis-je écrire un composant qui utilise un enregistreur sans dépendre d'une seule bibliothèque? Si c'est le cas require('my_logger_library'), les personnes utilisant mon composant devront remplacer le besoin d'utiliser leur propre bibliothèque. Au lieu de cela, je peux permettre aux gens de passer un rappel qui encapsule une implémentation de l'enregistreur dans la méthode "constructeur" ou "init" des composants. C'est le but de DI.
moteutsch
4
À partir de la mi-2014 - npmjs.org/package/proxyquire rend les dépendances « requises » triviales.
arcseldon
4
Je ne comprends pas, le remplacement de require dans un module ne le remplace pas dans un autre. Si j'ai défini require sur une fonction dans mon test, puis que le module soit testé, les instructions require dans l'objet à tester n'utilisent pas la fonction définie dans le module de test. Comment cela injecte-t-il des dépendances?
HMR
72

requireest le moyen de gérer les dépendances dans Node.js et il est sûrement intuitif et efficace, mais il a aussi ses limites.

Mon conseil est de jeter un œil à certains des conteneurs d'injection de dépendance disponibles aujourd'hui pour Node.js pour avoir une idée de leurs avantages / inconvénients. Certains d'entre eux sont:

Juste pour en nommer quelques-uns.

Maintenant, la vraie question est, que pouvez-vous réaliser avec un conteneur DI Node.js, par rapport à un simple require?

Avantages:

  • meilleure testabilité: les modules acceptent leurs dépendances en entrée
  • Inversion de contrôle: décidez comment câbler vos modules sans toucher au code principal de votre application.
  • un algorithme personnalisable pour résoudre les modules: les dépendances ont des identifiants "virtuels", généralement ils ne sont pas liés à un chemin sur le système de fichiers.
  • Meilleure extensibilité: activée par l'IoC et les identifiants "virtuels".
  • Autres trucs fantaisistes possibles:
    • Initialisation asynchrone
    • Gestion du cycle de vie des modules
    • Extensibilité du conteneur DI lui-même
    • Peut facilement implémenter des abstractions de niveau supérieur (par exemple AOP)

Les inconvénients:

  • Différent de l '"expérience" Node.js: ne pas utiliser donne requirel'impression de s'écarter de la façon de penser de Node.
  • La relation entre une dépendance et sa mise en œuvre n'est pas toujours explicite. Une dépendance peut être résolue au moment de l'exécution et influencée par divers paramètres. Le code devient plus difficile à comprendre et à déboguer
  • Temps de démarrage plus lent
  • Maturité (pour le moment): aucune des solutions actuelles n'est vraiment populaire pour le moment, donc pas autant de tutoriels, pas d'écosystème, pas de test de combat.
  • Certains conteneurs DI ne fonctionneront pas bien avec les regroupeurs de modules comme Browserify et Webpack.

Comme pour tout ce qui concerne le développement logiciel, le choix entre DI ou requiredépend de vos besoins, de la complexité de votre système et de votre style de programmation.

Mario
la source
3
Pensez-vous que la situation a considérablement changé depuis '09?
Juho Vepsäläinen
13
Voulez-vous dire depuis 10 jours? :)
Mario
2
Nooon. 9 déc ... J'aurais dû savoir.
Juho Vepsäläinen
4
J'ai "implémenté" DI en utilisant module.exports = function (deps) {} sorte de modèle. Oui, cela fonctionne, mais ce n'est pas tout à fait idéal.
Juho Vepsäläinen
3
les modules acceptent leurs dépendances en entrée et les dépendances ne sont pas des sons explicites pour moi comme une contradiction.
Anton Rudeshko
53

Je sais que ce fil est assez ancien à ce stade, mais je pensais que je ferais écho à mes réflexions à ce sujet. Le TL; DR est qu'en raison de la nature dynamique et non typée de JavaScript, vous pouvez en fait faire beaucoup de choses sans recourir au modèle d'injection de dépendance (DI) ou utiliser un framework DI. Cependant, à mesure qu'une application devient plus grande et plus complexe, DI peut certainement aider à la maintenabilité de votre code.

DI en C #

Pour comprendre pourquoi DI n'est pas un besoin aussi important en JavaScript, il est utile de regarder un langage fortement typé comme C #. (Excuses à ceux qui ne connaissent pas C #, mais cela devrait être assez facile à suivre.) Disons que nous avons une application qui décrit une voiture et son klaxon. Vous définiriez deux classes:

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();
    }
}

Il y a peu de problèmes avec l'écriture du code de cette façon.

  1. La Carclasse est étroitement couplée à l'implémentation particulière du klaxon dans la Hornclasse. Si nous voulons changer le type de klaxon utilisé par la voiture, nous devons modifier la Carclasse même si son utilisation du klaxon ne change pas. Cela rend également les tests difficiles car nous ne pouvons pas tester la Carclasse indépendamment de sa dépendance, la Hornclasse.
  2. La Carclasse est responsable du cycle de vie de la Hornclasse. Dans un exemple simple comme celui-ci, ce n'est pas un gros problème, mais dans les applications réelles, les dépendances auront des dépendances, qui auront des dépendances, etc. La Carclasse devrait être responsable de la création de l'arborescence complète de ses dépendances. Ce n'est pas seulement compliqué et répétitif, mais cela viole la "responsabilité unique" de la classe. Il devrait se concentrer sur le fait d'être une voiture, pas sur la création d'instances.
  3. Il n'y a aucun moyen de réutiliser les mêmes instances de dépendance. Encore une fois, ce n'est pas important dans cette application de jouet, mais envisagez une connexion à la base de données. Vous auriez généralement une seule instance qui est partagée dans votre application.

Maintenant, refactorisons ceci pour utiliser un modèle d'injection de dépendance.

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();
    }
}

Nous avons fait deux choses clés ici. Tout d'abord, nous avons introduit une interface que notre Hornclasse implémente. Cela nous permet de coder la Carclasse sur l'interface au lieu de l'implémentation particulière. Maintenant, le code peut accepter tout ce qui est implémenté IHorn. Deuxièmement, nous avons retiré l'instanciation du klaxon et l'avons Carplutôt transmise. Cela résout les problèmes ci-dessus et laisse à la fonction principale de l'application le soin de gérer les instances spécifiques et leurs cycles de vie.

Cela signifie que cela pourrait introduire un nouveau type de klaxon à utiliser sans toucher la Carclasse:

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

Le principal pourrait simplement injecter une instance de la FrenchHornclasse à la place. Cela simplifie également considérablement les tests. Vous pouvez créer une MockHornclasse à injecter dans le Carconstructeur pour vous assurer de tester uniquement la Carclasse de manière isolée.

L'exemple ci-dessus montre l'injection de dépendance manuelle. Typiquement DI est fait avec un framework (par exemple Unity ou Ninject dans le monde C #). Ces frameworks feront tout le câblage de dépendance pour vous en parcourant votre graphique de dépendance et en créant des instances selon les besoins.

La méthode Node.js standard

Voyons maintenant le même exemple dans Node.js. Nous diviserions probablement notre code en 3 modules:

// 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();

Parce que JavaScript n'est pas typé, nous n'avons pas le même couplage étroit que nous avions auparavant. Il n'y a pas besoin d'interfaces (elles n'existent pas non plus) car le carmodule tentera simplement d'appeler la honkméthode sur tout ce que le hornmodule exporte.

De plus, étant donné que Node requiremet tout en cache, les modules sont essentiellement des singletons stockés dans un conteneur. Tout autre module qui effectue un requiresur le hornmodule obtiendra exactement la même instance. Cela facilite le partage d'objets singleton comme les connexions à la base de données.

Maintenant, il y a toujours le problème que le carmodule est chargé de récupérer sa propre dépendance horn. Si vous vouliez que la voiture utilise un module différent pour son klaxon, vous devriez changer la requiredéclaration dans le carmodule. Ce n'est pas une chose très courante à faire, mais cela pose des problèmes avec les tests.

La manière habituelle de gérer le problème de test est avec proxyquire . En raison de la nature dynamique de JavaScript, proxyquire intercepte les appels pour exiger et retourner tous les talons / maquettes que vous fournissez à la place.

var proxyquire = require('proxyquire');
var hornStub = {
    honk: function () {
        console.log("test beep!");
    }
};

var car = proxyquire('./car', { './horn': hornStub });

// Now make test assertions on car...

C'est plus que suffisant pour la plupart des applications. Si cela fonctionne pour votre application, allez-y. Cependant, d'après mon expérience, les applications devenant plus grandes et plus complexes, il est plus difficile de maintenir ce code.

DI en JavaScript

Node.js est très flexible. Si vous n'êtes pas satisfait de la méthode ci-dessus, vous pouvez écrire vos modules en utilisant le modèle d'injection de dépendance. Dans ce modèle, chaque module exporte une fonction d'usine (ou un constructeur de classe).

// 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();

Ceci est très similaire à la méthode C # plus tôt dans la mesure où le index.jsmodule est responsable des cycles de vie et du câblage par exemple. Le test unitaire est assez simple car vous pouvez simplement passer des simulations / stubs aux fonctions. Encore une fois, si cela suffit pour votre application, allez-y.

Bolus DI Framework

Contrairement à C #, il n'y a pas de framework DI standard établi pour vous aider dans votre gestion des dépendances. Il existe un certain nombre de cadres dans le registre npm, mais aucun n'est largement adopté. Beaucoup de ces options ont déjà été citées dans les autres réponses.

Je n'étais pas particulièrement satisfait de l'une des options disponibles, j'ai donc écrit mon propre bolus . Bolus est conçu pour fonctionner avec du code écrit dans le style DI ci-dessus et essaie d'être très SEC et très simple. En utilisant exactement les mêmes modules car.jset horn.jsmodules ci-dessus, vous pouvez réécrire le index.jsmodule avec bolus comme:

// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");

var car = injector.resolve("car");
car.honkHorn();

L'idée de base est de créer un injecteur. Vous enregistrez tous vos modules dans l'injecteur. Ensuite, vous résolvez simplement ce dont vous avez besoin. Bolus parcourra le graphique des dépendances et créera et injectera des dépendances selon les besoins. Vous n'économisez pas beaucoup dans un exemple de jouet comme celui-ci, mais dans les grandes applications avec des arbres de dépendance complexes, les économies sont énormes.

Bolus prend en charge un tas de fonctionnalités astucieuses comme les dépendances optionnelles et les globaux de test, mais il y a deux avantages clés que j'ai vus par rapport à l'approche Node.js standard. Premièrement, si vous avez beaucoup d'applications similaires, vous pouvez créer un module npm privé pour votre base qui crée un injecteur et y enregistre des objets utiles. Ensuite, vos applications spécifiques peuvent ajouter, remplacer et résoudre au besoin, tout comme la façon dont AngularJSl'injecteur fonctionne. Deuxièmement, vous pouvez utiliser un bolus pour gérer divers contextes de dépendances. Par exemple, vous pouvez utiliser un middleware pour créer un injecteur enfant par demande, enregistrer l'ID utilisateur, l'ID de session, l'enregistreur, etc. sur l'injecteur ainsi que les modules qui en dépendent. Ensuite, résolvez ce dont vous avez besoin pour répondre aux demandes. Cela vous donne des instances de vos modules par demande et évite d'avoir à transmettre l'enregistreur, etc. à chaque appel de fonction de module.

Dave Johnson
la source
1
il est également vrai qu'il existe une alternative à proxyquiretelle que sinonqui vous permet de faire des simulations très concises, par exemple let readFileStub = sinon.stub(fs, 'readFile').yields(new Error('something went wrong'));, puis les appels suivants pour fs.readFileretourner une erreur jusqu'à ce que vous reveniez le stub via readFileStub.restore(). Personnellement, je trouve DI d'une utilisation douteuse car j'ai l'impression que cela nécessite presque l'utilisation de classes / objets, ce qui est une hypothèse douteuse étant donné les tendances fonctionnelles de javascript.
Kevin
Merci pour cette bonne réponse + détaillée. Je l'ai presque manqué, en lisant pour la première fois le titre DI en C # .
Konstantin A. Magg
1
Très bonne réponse. Je me demande ce que vous pensez en 2019. Pour les grands projets, comme une question de préférence personnelle, laquelle préférez-vous - DI / IoC dans le nœud, ou tout simplement stubbing / moqueur avec jest, rewire, proxyquire, etc.? Merci.
Jamie Corkhill
Grande réponse équilibrée! Je vous remercie.
Johnny Oshika
36

J'ai également écrit un module pour accomplir cela, il s'appelle rewire . Utilisez simplement npm install rewireet ensuite:

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!
});

J'ai été inspiré par l'injecteur de Nathan MacInnes mais j'ai utilisé une approche différente. Je n'utilise pas vmpour évaluer le module de test, en fait j'utilise les propres exigences du nœud. De cette façon, votre module se comporte exactement comme l'utilisation require()(sauf vos modifications). Le débogage est également entièrement pris en charge.

Johannes Ewald
la source
7
À partir de la mi-2014 - npmjs.org/package/proxyquire rend les dépendances « requises » triviales.
arcseldon
proxyquire est cool aussi! Ils utilisent maintenant le module "module" interne, ce qui est bien mieux que d'utiliser vm du nœud. Mais après tout, c'est juste une question de style. J'aime que mon module utilise l'exigence d'origine et permute les dépendances plus tard. De plus, le recâblage permet également de remplacer les globaux.
Johannes Ewald
Très intéressant à la recherche de quelque chose comme ça à utiliser au travail, ce module affecte-t-il également les modules en aval?
akst
for proxyquire Il est dit dans la description qu'il est utilisé pour les tests: «Les nœuds proxy nécessitent pour pouvoir autoriser les dépendances lors des tests . pas pour DI, non?
Marwen Trabelsi
17

J'ai construit Electrolyte dans ce but. Les autres solutions d'injection de dépendance là-bas étaient trop envahissantes à mon goût, et jouer avec le monde requireest un grief particulier à moi.

Electrolyte comprend des modules, en particulier ceux qui exportent une fonction de "configuration" comme vous le voyez dans le middleware Connect / Express. Essentiellement, ces types de modules ne sont que des usines pour certains objets qu'ils retournent.

Par exemple, un module qui crée une connexion à une base de données:

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' ];

Ce que vous voyez en bas sont des annotations , un bit supplémentaire de métadonnées qu'Electrolyte utilise pour instancier et injecter des dépendances, reliant automatiquement les composants de votre application.

Pour créer une connexion à une base de données:

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

L'électrolyte traverse de manière transitoire les @requiredépendances 'd et injecte des instances comme arguments à la fonction exportée.

La clé est que cela est peu invasif. Ce module est entièrement utilisable, indépendant de l'électrolyte lui-même. Cela signifie que vos tests unitaires peuvent tester uniquement le module testé , en passant des objets fictifs sans avoir besoin de dépendances supplémentaires pour recâbler les composants internes.

Lors de l'exécution de l'application complète, Electrolyte intervient au niveau inter-modules, câblant les choses ensemble sans avoir besoin de globales, de singletons ou de plomberie excessive.

Jared Hanson
la source
1
Pourriez-vous clarifier ce qui se passe dans le code que vous avez publié lors d'un appel à connect()lancer? Même si je ne connais pas l'API MySql pour Node, je m'attends à ce que cet appel soit asynchrone, donc l'illustration n'est pas tout à fait claire.
Andrey Agibalov
utilise actuellement l'électrolyte. Vous prétendez qu'il est facile d'INJECTER des modules via des exports ['@ require']. Mais si je dois écraser l'un des modules requis, comment est-ce réalisable dans l'électrolyte? Actuellement, si nous avons besoin de modules, cela peut être facilement réalisé. Mais pour l'électrolyte, c'est un énorme inconvénient .... Avez-vous des exemples où nous pouvons utiliser la version tronquée des modules et la passer dynamiquement pendant l'instanciation / ioc.use des cas de test. Donc, fondamentalement, dans le test unitaire, si nous pouvons faire ioc.create ('modulename') puis faire l'injection de modules dépendants (mais ceux qui sont
tronqués
1
Vous n'appeleriez pas à ioc.createpartir d'un test unitaire. Un test unitaire ne doit tester que le module testé et ne pas apporter d'autres dépendances, y compris l'électrolyte. En suivant ce conseil, vous feriezobjToTest = require('modulename')(mockObj1, mockObj2);
Jared Hanson
8

J'ai examiné cela moi-même. Je n'aime pas introduire des bibliothèques d'utilitaires de dépendance magique qui fournissent des mécanismes pour détourner les importations de mes modules. Au lieu de cela, je suis venu avec une «directive de conception» pour mon équipe pour indiquer de manière plutôt explicite quelles dépendances peuvent être moquées en introduisant une exportation de fonction d'usine dans mes modules.

J'utilise largement les fonctionnalités d'ES6 pour les paramètres et la déstructuration afin d'éviter un passe-partout et de fournir un mécanisme de remplacement de dépendance nommé.

Voici un exemple:

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();

Et voici un exemple de son utilisation

import { factory } from './bar';

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

Excusez la syntaxe ES6 pour ceux qui ne la connaissent pas.

ctrlplusb
la source
Très ingénieux en effet!
Arnold
4

J'ai récemment vérifié ce fil pour la même raison que l'OP - la plupart des bibliothèques que j'ai rencontrées réécrivent temporairement l'instruction require. J'ai eu des succès mitigés avec cette méthode, et j'ai donc fini par utiliser l'approche suivante.

Dans le cadre d'une application express - j'enveloppe app.js dans un fichier bootstrap.js:

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();

Le mappage d'objet transmis au chargeur ressemble à ceci:

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

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

Alors, plutôt que d'appeler directement, il faut ...

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

Si aucun alias n'est situé dans le chargeur - il sera par défaut simplement requis. Cela a deux avantages: je peux permuter dans n'importe quelle version de la classe, et cela supprime la nécessité d'utiliser des noms de chemin relatifs dans toute l'application (donc si j'ai besoin d'une bibliothèque personnalisée en dessous ou au-dessus du fichier actuel, je n'ai pas besoin de parcourir , et require mettra le module en cache avec la même clé). Cela me permet également de spécifier des simulations à tout moment dans l'application, plutôt que dans la suite de tests immédiate.

Je viens de publier un petit module npm pour plus de commodité:

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

sunwukung
la source
3

La réalité est que vous pouvez tester votre node.js sans conteneur IoC car JavaScript est un langage de programmation vraiment dynamique et vous pouvez modifier presque tout au moment de l'exécution.

Considérer ce qui suit:

import UserRepository from "./dal/user_repository";

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

export default UserController;

Vous pouvez donc remplacer le couplage entre les composants lors de l'exécution. J'aime à penser que nous devrions viser à découpler nos modules JavaScript.

Le seul moyen de réaliser un véritable découplage consiste à supprimer la référence à UserRepository:

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

export default UserController;

Cela signifie que quelque part ailleurs, vous devrez faire la composition de l'objet:

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

export default new UserController(new UserRepository());

J'aime l'idée de déléguer la composition de l'objet à un conteneur IoC. Vous pouvez en savoir plus sur cette idée dans l'article L'état actuel de l'inversion des dépendances en JavaScript . L'article tente de démystifier certains «mythes du conteneur JavaScript IoC»:

Mythe 1: il n'y a pas de place pour les conteneurs IoC en JavaScript

Mythe 2: Nous n'avons pas besoin de conteneurs IoC, nous avons déjà des chargeurs de modules!

Mythe 3: inversion des dépendances === injection de dépendances

Si vous aimez également l'idée d'utiliser un conteneur IoC, vous pouvez jeter un œil à InversifyJS. La dernière version (2.0.0) prend en charge de nombreux cas d'utilisation:

  • Modules du noyau
  • Middleware du noyau
  • Utilisez des classes, des littéraux de chaîne ou des symboles comme identificateurs de dépendance
  • Injection de valeurs constantes
  • Injection de constructeurs de classe
  • Injection d'usines
  • Usine automobile
  • Injection de fournisseurs (usine asynchrone)
  • Gestionnaires d'activation (utilisés pour injecter des proxys)
  • Multi injections
  • Liaisons balisées
  • Décorateurs d'étiquettes personnalisées
  • Liaisons nommées
  • Liaisons contextuelles
  • Exceptions amicales (par exemple, dépendances circulaires)

Vous pouvez en savoir plus à ce sujet sur InversifyJS .

Remo H. Jansen
la source
2

Pour ES6, j'ai développé ce conteneur https://github.com/zazoomauro/node-dependency-injection

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

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

Ensuite, vous pouvez définir, par exemple, le choix du transport dans le conteneur:

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

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

Cette classe est maintenant beaucoup plus flexible car vous avez séparé le choix du transport hors de l'implémentation et dans le conteneur.

Maintenant que le service de messagerie est dans le conteneur, vous pouvez l'injecter en tant que dépendance d'autres classes. Si vous avez une classe NewsletterManager comme celle-ci:

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

export default NewsletterManager

Lors de la définition du service newsletter_manager, le service mailer n'existe pas encore. Utilisez la classe Reference pour indiquer au conteneur d'injecter le service de messagerie lorsqu'il initialise le gestionnaire de newsletter:

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'))

Vous pouvez également configurer le conteneur avec des fichiers de configuration comme les fichiers Yaml, Json ou JS

Le conteneur de services peut être compilé pour diverses raisons. Ces raisons incluent la vérification de tout problème potentiel tel que les références circulaires et l'amélioration de l'efficacité du conteneur.

container.compile()
Mauro
la source
1

Cela dépend de la conception de votre application. Vous pouvez évidemment faire une injection de type Java où vous créez un objet d'une classe avec la dépendance passée dans le constructeur comme ceci.

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

var cache = new Cache(mysqlStore);

Si vous ne faites pas de POO en javascript, vous pouvez créer une fonction init qui configure tout.

Cependant, il existe une autre approche que vous pouvez adopter, qui est plus courante dans un système basé sur des événements tel que node.js. Si vous pouvez modéliser votre application pour qu'elle agisse uniquement (la plupart du temps) sur les événements, il vous suffit de tout configurer (ce que je fais habituellement en appelant une fonction init) et d'émettre des événements à partir d'un stub. Cela rend les tests assez faciles et lisibles.

Satyam Shekhar
la source
Merci pour votre réponse, mais je ne comprends pas bien votre deuxième partie de votre réponse.
Erik
1

J'ai toujours aimé la simplicité du concept IoC - "Vous n'avez rien à savoir sur l'environnement, vous serez appelé par quelqu'un en cas de besoin"

Mais toutes les implémentations IoC que j'ai vues ont fait exactement le contraire - elles encombrent le code avec encore plus de choses que sans lui. J'ai donc créé mon propre IoC qui fonctionne comme je le voudrais - il reste caché et invisible 90% du temps .

Il est utilisé dans le framework Web MonoJS http://monojs.org

Je parle de choses simples, comme partager un objet de connexion à une base de données, jusqu'à présent, mais je n'ai pas trouvé de solution qui me satisfasse.

C'est fait comme ceci - enregistrez le composant une fois dans la configuration.

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

Et utilisez-le partout

app.db.findSomething()

Vous pouvez voir le code de définition complet des composants (avec DB Connection et d'autres composants) ici https://github.com/sinizinairina/mono/blob/master/mono.coffee

C'est le seul endroit où vous devez dire à IoC quoi faire, après quoi tous ces composants seront créés et câblés automatiquement et vous n'aurez plus à voir le code spécifique IoC dans votre application.

L'IoC lui-même https://github.com/alexeypetrushin/miconjs

Alexey Petrushin
la source
6
Bien qu'annoncé comme un DI, cela ressemble beaucoup plus à un localisateur de services.
KyorCode
2
Ça a l'air génial, dommage que ce soit en coffret seulement
Rafael P. Miranda
1

Je pense que nous avons encore besoin de l'injection de dépendances dans Nodejs car cela desserre les dépendances entre les services et rend l'application plus claire.

Inspiré par Spring Framework , j'implémente également mon propre module pour prendre en charge l'injection de dépendances dans Nodejs. Mon module est également capable de détecter les code changeset auto reloadles services sans redémarrer votre application.

Visitez mon projet à: Buncha - Conteneur IoC

Je vous remercie!

Tho
la source
0

J'ai travaillé avec .Net, PHP et Java pendant longtemps, donc je voulais aussi avoir une injection de dépendance pratique dans NodeJS. Les gens ont dit que la DI intégrée dans NodeJS était suffisante car nous pouvons l'obtenir avec Module. Mais cela ne m'a pas bien satisfait. Je ne voulais pas garder un module plus qu'une classe. De plus, je voulais que la DI ait un support complet pour la gestion du cycle de vie du module (module singleton, module transitoire, etc.) mais avec le module Node, je devais écrire du code manuel très souvent. Enfin, je voulais faciliter le test unitaire. C'est pourquoi je me suis créé une injection de dépendance.

Si vous recherchez une DI, essayez-la. Il peut être trouvé ici: https://github.com/robo-creative/nodejs-robo-container . C'est entièrement documenté. Il aborde également certains problèmes courants avec DI et comment les résoudre de manière OOP. J'espère que ça aide.

Robo
la source
Oui, vous avez raison, une bibliothèque DI dans vos projets est importante pour de bonnes architectures.Si vous voulez voir un cas d'utilisation pour DI, consultez le fichier Lisez-moi de ce référentiel également une bibliothèque DI pour le nœud Jems DI .
Francisco Mercedes
-1

J'ai récemment créé une bibliothèque appelée circuitbox qui vous permet d'utiliser l'injection de dépendance avec node.js. Il fait une véritable injection de dépendance par rapport à la plupart des bibliothèques basées sur la recherche de dépendance que j'ai vues. Circuitbox prend également en charge les routines de création et d'initialisation asynchrones. Voici un exemple:

Supposons que le code suivant se trouve dans un fichier appelé 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;

Supposons que ce qui suit se trouve dans le fichier main.js

'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');
});

Circuitbox vous permet de définir vos composants et de déclarer leurs dépendances en tant que modules. Une fois initialisé, il vous permet de récupérer un composant. Circuitbox injecte automatiquement tous les composants requis par le composant cible et vous les donne pour utilisation.

Le projet est en version alpha. Vos commentaires, idées et retours sont les bienvenus.

J'espère que ça aide!

oddjobsman
la source
-1

Je pense que d'autres articles ont fait un excellent travail dans l'argument de l'utilisation de DI. Pour moi, les raisons sont

  1. Injectez des dépendances sans connaître leur chemin. Cela signifie que si vous changez l'emplacement d'un module sur le disque ou que vous le permutez avec un autre, vous n'avez pas besoin de toucher tous les fichiers qui en dépendent.

  2. Il est beaucoup plus facile de se moquer des dépendances pour les tests sans avoir à remplacer la requirefonction globale d'une manière qui fonctionne sans problème.

  3. Il vous aide à organiser et à raisonner votre application en tant que modules à couplage lâche.

Mais j'ai eu beaucoup de mal à trouver un framework DI que mon équipe et moi pouvons facilement adopter. J'ai donc récemment construit un framework appelé deppie basé sur ces fonctionnalités

  • API minimale qui peut être apprise en quelques minutes
  • Aucun code / configuration / annotations supplémentaires requis
  • Mappage direct un à un avec les requiremodules
  • Peut être adopté partiellement pour fonctionner avec le code existant
gafi
la source
-1

Il devrait être flexible et simple comme ceci:

var MyClass1 = function () {}
var MyClass2 = function (myService1) {
    // myService1.should.be.instanceof(MyClass1); 
}


container.register('myService1', MyClass1);
container.register('myService2', MyClass2, ['myService1']);

J'ai écrit un article sur l'injection de dépendances dans node.js.

J'espère que cela peut vous aider.

slava
la source
-1

Node.js nécessite autant de DI que n'importe quelle autre plateforme. Si vous construisez quelque chose de gros, DI vous permettra de vous moquer plus facilement des dépendances de votre code et de tester votre code à fond.

Vos modules de couche de base de données, par exemple, ne devraient pas seulement être requis au niveau de vos modules de code métier car, lors du test unitaire de ces modules de code métier, les daos se chargeront et se connecteront à la base de données.

Une solution serait de passer les dépendances en paramètres de module:

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

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

De cette façon, les dépendances peuvent être simulées facilement et naturellement et vous pouvez rester concentré sur le test de votre code, sans utiliser de bibliothèque tierce compliquée.

Il existe d'autres solutions (Broadway, architecte, etc.) qui peuvent vous aider. bien qu'ils puissent faire plus que ce que vous voulez ou utiliser plus d'encombrement.

user2468170
la source
Presque via l'évolution naturelle, j'ai fini par faire de même. Je passe une dépendance en tant que paramètres et cela fonctionne très bien pour les tests.
munkee
-1

J'ai développé une bibliothèque qui gère l'injection de dépendances d'une manière simple, qui diminue le code passe-partout. Chaque module est défini par un nom unique et une fonction de contrôleur. Les paramètres du contrôleur reflètent les dépendances du module.

En savoir plus sur KlarkJS

Bref exemple:

KlarkModule(module, 'myModuleName1', function($nodeModule1, myModuleName2) {
    return {
        log: function() { console.log('Hello from module myModuleName1') }
    };
});
  • myModuleName1 est le nom du module.
  • $nodeModule1est une bibliothèque externe de node_module. Le nom se résout en node-module1. Le préfixe $indique qu'il s'agit d'un module externe.
  • myModuleName2 est le nom d'un module interne.
  • La valeur de retour du contrôleur est utilisée à partir des autres modules internes, lorsqu'ils définissent le paramètre myModuleName1.
Apostolidis
la source
-1

J'ai découvert cette question en répondant à un problème sur mon propre module DI demandant pourquoi on aurait besoin d'un système DI pour la programmation NodeJS.

La réponse tendait clairement à celles données dans ce fil: cela dépend. Il existe des compromis pour les deux approches et la lecture des réponses à cette question en donne une bonne forme.

Donc, la vraie réponse à cette question devrait être que dans certaines situations, vous utiliseriez un système DI, dans d'autres non.

Cela dit, ce que vous voulez en tant que développeur, c'est de ne pas vous répéter et de réutiliser vos services dans vos différentes applications.

Cela signifie que nous devons écrire des services prêts à être utilisés dans le système DI mais non liés aux bibliothèques DI. Pour moi, cela signifie que nous devrions écrire des services comme celui-ci:

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
}

De cette façon, votre service fonctionne, peu importe si vous l'utilisez avec ou sans outil DI.

nfroidure
la source
-1

TypeDI est le plus doux de tous mentionné ici, regardez ce code dans TypeDI

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

@Service()
class SomeClass {

    someMethod() {
    }

}

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

Regardez aussi ce code:

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;

}
ahmadalibaloch
la source