Sé que este hilo es bastante antiguo en este punto, pero pensé que me gustaría intervenir con mis pensamientos sobre esto. El TL; DR es que debido a la naturaleza dinámica y sin tipo de JavaScript, en realidad puede hacer mucho sin recurrir al patrón de inyección de dependencia (DI) o usar un marco DI. Sin embargo, a medida que una aplicación se hace más grande y más compleja, DI definitivamente puede ayudar a la mantenibilidad de su código.
DI en C #
Para entender por qué DI no es una necesidad tan grande en JavaScript, es útil mirar un lenguaje fuertemente tipado como C #. (Disculpas a aquellos que no conocen C #, pero debería ser lo suficientemente fácil de seguir). Digamos que tenemos una aplicación que describe un automóvil y su bocina. Definirías dos clases:
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();
}
}
Hay pocos problemas al escribir el código de esta manera.
- La
Car
clase está estrechamente vinculada a la implementación particular de la bocina en la Horn
clase. Si queremos cambiar el tipo de bocina utilizada por el automóvil, tenemos que modificar la Car
clase a pesar de que su uso de la bocina no cambia. Esto también dificulta las pruebas porque no podemos probar la Car
clase aisladamente de su dependencia, la Horn
clase.
- La
Car
clase es responsable del ciclo de vida de la Horn
clase. En un ejemplo simple como este no es un gran problema, pero en aplicaciones reales las dependencias tendrán dependencias, que tendrán dependencias, etc.Car
clase debería ser responsable de crear el árbol completo de sus dependencias. Esto no solo es complicado y repetitivo, sino que viola la "responsabilidad única" de la clase. Debería centrarse en ser un automóvil, no en crear instancias.
- No hay forma de reutilizar las mismas instancias de dependencia. Nuevamente, esto no es importante en esta aplicación de juguete, pero considere una conexión de base de datos. Por lo general, tendría una única instancia compartida en su aplicación.
Ahora, refactoricemos esto para usar un patrón de inyección de dependencia.
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();
}
}
Hemos hecho dos cosas clave aquí. Primero, hemos introducido una interfaz que Horn
implementa nuestra clase. Esto nos permite codificar la Car
clase en la interfaz en lugar de la implementación particular. Ahora el código podría tomar cualquier cosa que implemente IHorn
. En segundo lugar, hemos sacado la instanciación de la bocina y la hemos pasado Car
. Esto resuelve los problemas anteriores y deja a la función principal de la aplicación administrar las instancias específicas y sus ciclos de vida.
Lo que esto significa es que podría introducir un nuevo tipo de bocina para que el automóvil use sin tocar la Car
clase:
class FrenchHorn : IHorn
{
public void Honk()
{
Console.WriteLine("le beep!");
}
}
El principal podría simplemente inyectar una instancia de la FrenchHorn
clase en su lugar. Esto también simplifica dramáticamente las pruebas. Puede crear una MockHorn
clase para inyectar en el Car
constructor para asegurarse de que está probando solo elCar
clase de forma aislada.
El ejemplo anterior muestra la inyección manual de dependencia. Por lo general, DI se realiza con un marco (por ejemplo, Unity o Ninject en el mundo C #). Estos marcos harán todo el cableado de dependencia por usted al recorrer su gráfico de dependencia y crear instancias según sea necesario.
La forma estándar de Node.js
Ahora veamos el mismo ejemplo en Node.js. Probablemente dividiríamos nuestro código en 3 módulos:
// 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();
Debido a que JavaScript no está tipificado, no tenemos el mismo acoplamiento estrecho que teníamos antes. No hay necesidad de interfaces (ni existen) ya que el car
módulo solo intentará llamar al honk
método en cualquierhorn
que exporta módulo.
Además, debido a que Node's require
almacena en caché todo, los módulos son esencialmente singletons almacenados en un contenedor. Cualquier otro módulo que realice un require
en el horn
módulo obtendrá exactamente la misma instancia. Esto hace que compartir objetos singleton como conexiones de bases de datos sea muy fácil.
Ahora todavía existe el problema de que el car
módulo es responsable de recuperar su propia dependencia horn
. Si quisieras que el auto usara un módulo diferente para su bocina, tendrías que cambiar la require
declaración en el car
módulo. Esto no es algo muy común, pero causa problemas con las pruebas.
La forma habitual en que las personas manejan el problema de las pruebas es con proxyquire . Debido a la naturaleza dinámica de JavaScript, proxyquire intercepta las llamadas para exigir y devuelve los resguardos / simulacros que proporcione en su lugar.
var proxyquire = require('proxyquire');
var hornStub = {
honk: function () {
console.log("test beep!");
}
};
var car = proxyquire('./car', { './horn': hornStub });
// Now make test assertions on car...
Esto es más que suficiente para la mayoría de las aplicaciones. Si funciona para tu aplicación, ve con ella. Sin embargo, en mi experiencia a medida que las aplicaciones se hacen más grandes y complejas, mantener un código como este se vuelve más difícil.
DI en JavaScript
Node.js es muy flexible. Si no está satisfecho con el método anterior, puede escribir sus módulos utilizando el patrón de inyección de dependencia. En este patrón, cada módulo exporta una función de fábrica (o un constructor de clase).
// 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();
Esto es muy análogo al método C # anterior en que el index.js
módulo es responsable, por ejemplo, de los ciclos de vida y el cableado. Las pruebas unitarias son bastante simples, ya que puede pasar simulaciones / apéndices a las funciones. Nuevamente, si esto es lo suficientemente bueno para su aplicación, hágalo.
Marco Bolus DI
A diferencia de C #, no hay marcos DI estándar establecidos para ayudarlo con su gestión de dependencias. Existen varios marcos en el registro npm, pero ninguno tiene una adopción generalizada. Muchas de estas opciones ya se han citado en las otras respuestas.
No estaba particularmente satisfecho con ninguna de las opciones disponibles, así que escribí mi propio bolo llamado . Bolus está diseñado para funcionar con código escrito en el estilo DI anterior e intenta ser muy SECO y muy simple. Usando exactamente lo mismo car.js
y los horn.js
módulos anteriores, puede reescribir el index.js
módulo con bolo como:
// index.js
var Injector = require("bolus");
var injector = new Injector();
injector.registerPath("**/*.js");
var car = injector.resolve("car");
car.honkHorn();
La idea básica es crear un inyector. Registra todos sus módulos en el inyector. Entonces simplemente resuelves lo que necesitas. Bolus recorrerá el gráfico de dependencia y creará e inyectará dependencias según sea necesario. No se ahorra mucho en un ejemplo de juguete como este, pero en aplicaciones grandes con árboles de dependencia complicados, los ahorros son enormes.
Bolus admite un montón de características ingeniosas como dependencias opcionales y pruebas globales, pero hay dos beneficios clave que he visto en relación con el enfoque estándar de Node.js. Primero, si tiene muchas aplicaciones similares, puede crear un módulo npm privado para su base que cree un inyector y registre objetos útiles en él. Luego, sus aplicaciones específicas pueden agregar, anular y resolver según sea necesario, de manera similar a cómo AngularJSinyector funciona. En segundo lugar, puede usar el bolo para administrar varios contextos de dependencias. Por ejemplo, podría usar middleware para crear un inyector secundario por solicitud, registrar la identificación de usuario, la identificación de sesión, el registrador, etc. en el inyector junto con los módulos que dependen de ellos. Luego resuelva lo que necesita para atender las solicitudes. Esto le proporciona instancias de sus módulos por solicitud y evita tener que pasar el registrador, etc. a cada llamada de función del módulo.