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.
- La
Car
classe est étroitement couplée à l'implémentation particulière du klaxon dans la Horn
classe. Si nous voulons changer le type de klaxon utilisé par la voiture, nous devons modifier la Car
classe même si son utilisation du klaxon ne change pas. Cela rend également les tests difficiles car nous ne pouvons pas tester la Car
classe indépendamment de sa dépendance, la Horn
classe.
- La
Car
classe est responsable du cycle de vie de la Horn
classe. 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 Car
classe 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.
- 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 Horn
classe implémente. Cela nous permet de coder la Car
classe 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 Car
plutô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 Car
classe:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
Le principal pourrait simplement injecter une instance de la FrenchHorn
classe à la place. Cela simplifie également considérablement les tests. Vous pouvez créer une MockHorn
classe à injecter dans le Car
constructeur pour vous assurer de tester uniquement la Car
classe 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 car
module tentera simplement d'appeler la honk
méthode sur tout ce que le horn
module exporte.
De plus, étant donné que Node require
met tout en cache, les modules sont essentiellement des singletons stockés dans un conteneur. Tout autre module qui effectue un require
sur le horn
module 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 car
module 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 require
déclaration dans le car
module. 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.js
module 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.js
et horn.js
modules ci-dessus, vous pouvez réécrire le index.js
module 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.