Inyección de dependencias de Javascript y DIP en el nodo: requiere vs inyección de constructor

Soy nuevo en el desarrollo de NodeJs proveniente del mundo .NET. Estoy buscando en la web las mejores prácticas para la calificación DI / DIP en Javascript

En .NET declararía mis dependencias en el constructor, mientras que en javascript veo que un patrón común es declarar dependencias en el nivel del módulo a través de una statement de requerimiento.

para mí, parece que cuando uso require, estoy acoplado a un archivo específico mientras uso un constructor para recibir mi dependencia es más flexible.

¿Qué recomendarías hacer como una mejor práctica en javascript? (Estoy buscando el patrón arquitectónico y no una solución técnica del COI)

buscando en la web llegué a lo largo de esta publicación de blog (que tiene una discusión muy interesante en los comentarios): https://blog.risingstack.com/dependency-injection-in-node-js/

eso verifica mi conflicto bastante bien. Aquí hay algo de código de la publicación del blog para que entiendas de qué estoy hablando:

// team.js var User = require('./user'); function getTeam(teamId) { return User.find({teamId: teamId}); } module.exports.getTeam = getTeam; 

Una prueba simple se vería algo como esto:

  // team.spec.js var Team = require('./team'); var User = require('./user'); describe('Team', function() { it('#getTeam', function* () { var users = [{id: 1, id: 2}]; this.sandbox.stub(User, 'find', function() { return Promise.resolve(users); }); var team = yield team.getTeam(); expect(team).to.eql(users); }); }); 

VS DI:

 // team.js function Team(options) { this.options = options; } Team.prototype.getTeam = function(teamId) { return this.options.User.find({teamId: teamId}) } function create(options) { return new Team(options); } 

prueba:

 // team.spec.js var Team = require('./team'); describe('Team', function() { it('#getTeam', function* () { var users = [{id: 1, id: 2}]; var fakeUser = { find: function() { return Promise.resolve(users); } }; var team = Team.create({ User: fakeUser }); var team = yield team.getTeam(); expect(team).to.eql(users); }); }); 

Con respecto a su pregunta: No creo que exista una práctica común en la comunidad de JS. He visto ambos tipos en la naturaleza, requieren modificaciones (como rewire o proxyquire ) e inyección de constructor (a menudo utilizando un contenedor DI dedicado). Sin embargo, personalmente, creo que no usar un contenedor DI es un mejor ajuste para JS. Y eso es porque JS es un lenguaje dynamic con funciones como ciudadanos de primera clase . Déjame explicarte que:

El uso de contenedores DI impone la inyección del constructor para todo . Crea una sobrecarga de configuración enorme por dos razones principales:

  1. Proporcionar simulacros en pruebas unitarias.
  2. Creando componentes abstractos que no saben nada de su entorno.

Con respecto al primer argumento : no ajustaría mi código solo para mis pruebas de unidad. Si hace que su código sea más limpio, más simple, más versátil y menos propenso a errores, entonces salga. Pero si su única razón es su prueba de unidad, no tomaría la compensación. Puedes llegar bastante lejos con modificaciones requeridas y parches de mono . Y si te encuentras escribiendo demasiados simulacros, probablemente no deberías escribir una prueba de unidad, sino una prueba de integración. Eric Elliott ha escrito un gran artículo sobre este problema.

Respecto al segundo argumento : Este es un argumento válido. Si desea crear un componente que solo se preocupa por una interfaz, pero no sobre la implementación real, optaría por una simple inyección de constructor. Sin embargo, dado que JS no te obliga a usar clases para todo, ¿por qué no usar funciones?

En la progtwigción funcional , separar la E / S con estado del procesamiento real es un paradigma común. Por ejemplo, si está escribiendo código que se supone que cuenta los tipos de archivos en una carpeta, uno podría escribir esto (especialmente cuando él / ella viene de un idioma que impone clases en todas partes):

 const fs = require("fs"); class FileTypeCounter { countFileTypes(dirname, callback) { fs.readdir(dirname, function (err) { if (err) return callback(err); // recursively walk all folders and count file types // ... callback(null, fileTypes); }); } } 

Ahora, si desea probar eso, necesita cambiar su código para inyectar un módulo falso de fs :

 class FileTypeCounter { constructor(fs) { this.fs = fs; } countFileTypes(dirname, callback) { this.fs.readdir(dirname, function (err) { // ... }); } } 

Ahora, todos los que están usando tu clase necesitan inyectar fs en el constructor. Ya que esto es aburrido y hace que su código sea más complicado una vez que tenga gráficos de dependencia largos, los desarrolladores inventaron los contenedores DI donde solo pueden configurar cosas y el contenedor DI se da cuenta de la creación de instancias.

Sin embargo, ¿qué hay de escribir funciones puras?

 function fileTypeCounter(allFiles) { // count file types return fileTypes; } function getAllFilesInDir(dirname, callback) { // recursively walk all folders and collect all files // ... callback(null, allFiles); } // now let's compose both functions function getAllFileTypesInDir(dirname, callback) { getAllFilesInDir(dirname, (err, allFiles) => { callback(err, !err && fileTypeCounter(allFiles)); }); } 

Ahora tiene dos funciones súper versátiles listas para usar, una que hace IO y la otra que procesa los datos. fileTypeCounter es una función pura y super fácil de probar. getAllFilesInDir es impuro pero como una tarea tan común, a menudo lo encontrarás ya en npm donde otras personas han escrito pruebas de integración para él. getAllFileTypesInDir simplemente compone sus funciones con un poco de flujo de control. Este es un caso típico para una prueba de integración en la que desea asegurarse de que toda la aplicación funciona correctamente.

Al separar su código entre IO y el procesamiento de datos, no encontrará la necesidad de inyectar nada. Y si no necesita inyectarse nada, es una buena señal. Las funciones puras son lo más fácil de probar y siguen siendo la forma más fácil de compartir código entre proyectos.

En el pasado, los contenedores DI como los conocemos de Java y .NET no existían. Con el Nodo 6 llegaron Proxies ES6 que abrieron la posibilidad de tales contenedores, por ejemplo, Awilix .

Así que vamos a reescribir su código para el ES6 moderno.

 class Team { constructor ({ User }) { this.User = user } getTeam (teamId) { return this.User.find({ teamId: teamId }) } } 

Y la prueba:

 import Team from './Team' describe('Team', function() { it('#getTeam', async function () { const users = [{id: 1, id: 2}] const fakeUser = { find: function() { return Promise.resolve(users) } } const team = new Team({ User: fakeUser }) const team = await team.getTeam() expect(team).to.eql(users) }) }) 

Ahora, usando Awilix, escribamos nuestra raíz de composición :

 import { createContainer, asClass } from 'awilix' import Team from './Team' import User from './User' const container = createContainer() .register({ Team: asClass(Team), User: asClass(User) }) // Grab an instance of Team const team = container.resolve('Team') // Alternatively... const team = container.cradle.Team // Use it team.getTeam(123) // calls User.find() 

Eso es tan simple como se pone; Awilix también puede manejar la vida útil de los objetos, al igual que los contenedores .NET / Java. Esto le permite hacer cosas geniales como inyectar al usuario actual a sus servicios, intántelo una vez por solicitud http, etc.