¿Necesito una inyección de dependencia en NodeJS o cómo lidiar con...?

Resuelto Erik asked hace 12 años • 22 respuestas

Actualmente estoy creando algunos proyectos experimentales con nodejs. He programado muchas aplicaciones web Java EE con Spring y aprecié la facilidad de inyección de dependencias allí.

Ahora tengo curiosidad: ¿Cómo hago la inyección de dependencia con el nodo? O: ¿Lo necesito siquiera? ¿Existe algún concepto que lo reemplace porque el estilo de programación es diferente?

Estoy hablando de cosas simples, como compartir un objeto de conexión a una base de datos, hasta ahora, pero no he encontrado una solución que me satisfaga.

Erik avatar Feb 13 '12 00:02 Erik
Aceptado

En resumen, no necesita un contenedor de inyección de dependencias ni un localizador de servicios como lo haría en C#/Java. Dado que Node.js aprovecha module pattern, no es necesario realizar una inyección de constructor o de propiedad. Aunque todavía puedes.

Lo bueno de JS es que puedes modificar casi cualquier cosa para lograr lo que deseas. Esto resulta útil a la hora de realizar pruebas.

He aquí mi ejemplo artificial, muy poco convincente.

MyClass.js:

var fs = require('fs');

MyClass.prototype.errorFileExists = function(dir) {
    var dirsOrFiles = fs.readdirSync(dir);
    for (var d of 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
    });
});

¿Observa cómo MyClassdepende del fsmódulo? Como mencionó @ShatyemShekhar, de hecho puedes realizar inyección de constructor o propiedad como en otros lenguajes. Pero no es necesario en Javascript.

En este caso, puedes hacer dos cosas.

Puede utilizar un código auxiliar para el fs.readdirSyncmétodo o puede devolver un módulo completamente diferente cuando llame a require.

Método 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étodo 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 clave es aprovechar el poder de Node.js y Javascript. Tenga en cuenta que soy un tipo de CoffeeScript, por lo que mi sintaxis JS podría ser incorrecta en alguna parte. Además, no digo que ésta sea la mejor manera, pero es una manera. Los gurús de Javascript podrían aportar otras soluciones.

Actualizar:

Esto debería abordar su pregunta específica sobre las conexiones de bases de datos. Crearía un módulo separado para encapsular la lógica de conexión de su base de datos. Algo como esto:

MyDbConnection.js: (asegúrate de elegir un nombre mejor)

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    
}

Luego, cualquier módulo que necesite una conexión a la base de datos simplemente incluirá su MyDbConnectionmódulo.

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. 

No sigas este ejemplo al pie de la letra. Es un ejemplo poco convincente de intentar comunicar que aprovecha el modulepatrón para gestionar sus dependencias. Ojalá esto ayude un poco más.

JP Richardson avatar Feb 13 '2012 02:02 JP Richardson

Sé que este hilo es bastante antiguo en este momento, pero pensé en intervenir con mis pensamientos al respecto. El TL;DR es que, debido a la naturaleza dinámica y sin tipo de JavaScript, en realidad puedes hacer muchas cosas sin recurrir al patrón de inyección de dependencia (DI) o usar un marco DI. Sin embargo, a medida que una aplicación crece y se vuelve más compleja, DI definitivamente puede ayudar a mantener el código.

DI en C#

Para comprender por qué DI no es una necesidad tan grande en JavaScript, es útil observar un lenguaje fuertemente tipado como C#. (Disculpas a aquellos que no saben C#, pero debería ser bastante 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 algunos problemas al escribir el código de esta manera.

  1. La Carclase está estrechamente ligada a la implementación particular de la bocina en la Hornclase. Si queremos cambiar el tipo de bocina que usa el auto, tenemos que modificar la Carclase aunque el uso de la bocina no cambie. Esto también dificulta las pruebas porque no podemos probar la Carclase aislada de su dependencia, la Hornclase.
  2. La Carclase es responsable del ciclo de vida de la Hornclase. En un ejemplo simple como este no es un gran problema, pero en aplicaciones reales las dependencias tendrán dependencias, las cuales tendrán dependencias, etc. La Carclase tendría que ser responsable de crear todo el árbol de sus dependencias. Esto no sólo es complicado y repetitivo, sino que viola la "responsabilidad única" de la clase. Debería centrarse en ser un coche, no en crear instancias.
  3. 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 a una base de datos. Normalmente, tendrá una única instancia compartida en toda 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 nuestra Hornclase implementa. Esto nos permite codificar la Carclase en la interfaz en lugar de la implementación particular. Ahora el código podría tomar cualquier cosa que se implemente IHorn. En segundo lugar, eliminamos la instanciación de la bocina Cary la pasamos. Esto resuelve los problemas anteriores y deja que la función principal de la aplicación administre las instancias específicas y sus ciclos de vida.

Lo que esto significa es que podríamos introducir un nuevo tipo de bocina para que el auto la use sin tocar la Carclase:

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

En su lugar , el principal podría simplemente inyectar una instancia de la FrenchHornclase. Esto también simplifica drásticamente las pruebas. Podrías crear una MockHornclase para inyectarla en el Carconstructor y asegurarte de que estás probando solo la Carclase de forma aislada.

El ejemplo anterior muestra la inyección de dependencia manual. Normalmente, la 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.

El método 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 tiene tipo, no tenemos el mismo acoplamiento estrecho que teníamos antes. No hay necesidad de interfaces (ni existen), ya que el carmódulo simplemente intentará llamar al honkmétodo en lo que sea que hornexporte el módulo.

Además, debido a que Node requirealmacena todo en caché, los módulos son esencialmente singletons almacenados en un contenedor. Cualquier otro módulo que realice una acción requireen el hornmódulo obtendrá exactamente la misma instancia. Esto hace que compartir objetos únicos, como conexiones de bases de datos, sea muy fácil.

Ahora todavía existe el problema de que el carmódulo es responsable de buscar su propia dependencia horn. Si quisieras que el auto usara un módulo diferente para su bocina, tendrías que cambiar la requiredeclaración en el carmódulo. Esto no es algo muy común, pero causa problemas con las pruebas.

La forma habitual en que la gente maneja el problema de las pruebas es con proxyquire . Debido a la naturaleza dinámica de JavaScript, proxyquire intercepta llamadas a require y devuelve cualquier código auxiliar o simulacro que usted proporcione.

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 aplicaciones. Si funciona para tu aplicación, hazlo. Sin embargo, según mi experiencia, a medida que las aplicaciones crecen y se vuelven más 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 el sentido de que el index.jsmódulo es responsable, por ejemplo, de los ciclos de vida y el cableado. Las pruebas unitarias son bastante simples, ya que puede simplemente pasar simulacros/stubs a las funciones. Nuevamente, si esto es lo suficientemente bueno para su aplicación, hágalo.

Marco de bolo DI

A diferencia de C#, no existen marcos DI estándar establecidos para ayudar con la gestión de dependencias. Hay varios marcos en el registro de 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í la mía propia llamada bolo . 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 los mismos car.jsmódulos horn.jsanteriores, puede reescribir el index.jsmódulo con bolo como:

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

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

The basic idea is that you create an injector. You register all of your modules in the injector. Then you simply resolve what you need. Bolus will walk the dependency graph and create and inject dependencies as needed. You don't save much in a toy example like this, but in large applications with complicated dependency trees the savings are huge.

Bolus supports a bunch of nifty features like optional dependencies and test globals, but there are two key benefits I've seen relative to the standard Node.js approach. First, if you have a lot of similar applications, you can create a private npm module for your base that creates an injector and registers useful objects on it. Then your specific apps can add, override, and resolve as needed much like how AngularJS's injector works. Second, you can use bolus to manage various contexts of dependencies. For example, you could use middleware to create a child injector per request, register the user id, session id, logger, etc. on the injector along with any modules depending on those. Then resolve what you need to serve requests. This gives you instances of your modules per request and prevents having to pass the logger, etc. along to every module function call.

Dave Johnson avatar Feb 20 '2016 20:02 Dave Johnson