Análisis de los enfoques de enlace de módulos en Node.js

Muchos desarrolladores de Node.js usan dependencias rígidas de módulos (exclusivamente) que usan require () para vincular módulos, pero hay otros enfoques con sus pros y sus contras. Hablaré de ellos en este artículo. Se considerarán cuatro enfoques:

  • Dependencias duras (require ())
  • Inyección de dependencia
  • Servicio de localización
  • Contenedores de dependencia integrados (contenedor DI)

Un poco sobre módulos


Los módulos y la arquitectura modular son la base de Node.js. Los módulos proporcionan encapsulación (ocultando detalles de implementación y abriendo solo la interfaz usando module.exports), reutilización de código, división lógica de código en archivos. Casi todas las aplicaciones de Node.js consisten en muchos módulos que de alguna manera deben interactuar. Si vincula incorrectamente los módulos o incluso deja que la interacción de los módulos se desvíe, entonces puede encontrar rápidamente que la aplicación comienza a "desmoronarse": los cambios en el código en un lugar conducen a una falla en otro, y las pruebas unitarias se vuelven simplemente imposibles. Idealmente, los módulos deberían tener una alta conectividad , pero un bajo acoplamiento .

Adicciones duras


Se produce una fuerte dependencia de un módulo con respecto a otro cuando se utiliza require (). Este es un enfoque efectivo, simple y común. Por ejemplo, solo queremos conectar el módulo responsable de interactuar con la base de datos:

// ourModule.js const db = require('db'); //    ... 

Pros:


  • Simplicidad
  • Organización visual de los módulos.
  • Depuración fácil

Contras:


  • Dificultad para reutilizar el módulo (por ejemplo, si queremos usar nuestro módulo repetidamente, pero con una instancia diferente de la base de datos)
  • Dificultad para la prueba unitaria (debe crear una instancia de base de datos ficticia y de alguna manera pasarla al módulo)

Resumen:


El enfoque es bueno para pequeñas aplicaciones o prototipos, así como para conectar módulos sin estado: fábricas, diseñadores y conjuntos de características.

Inyección de dependencia


La idea principal de la inyección de dependencias es transferir dependencias de un componente externo al módulo. Por lo tanto, se elimina la dependencia dura en el módulo y es posible reutilizarlo en diferentes contextos (por ejemplo, con diferentes instancias de base de datos).

La inyección de dependencias se puede implementar pasando la dependencia en el argumento del constructor o estableciendo las propiedades del módulo, pero en la práctica es mejor usar el primer método. Apliquemos la implementación de dependencias en la práctica creando una instancia de la base de datos usando la fábrica y pasándola a nuestro módulo:

 // ourModule.js module.exports = (db) => { //       ... }; 

Módulo externo:

 const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance); 

Ahora no solo podemos reutilizar nuestro módulo, sino también escribir fácilmente una prueba unitaria para él: simplemente cree un objeto simulado para la instancia de la base de datos y páselo al módulo.

Pros:


  • Facilidad de redacción de pruebas unitarias
  • Aumenta la reutilización de los módulos.
  • Disminución del compromiso, mayor conectividad
  • Cambiar la responsabilidad de crear dependencias a un nivel superior: a menudo esto mejora la legibilidad del programa, ya que las dependencias importantes se recopilan en un solo lugar y no se extienden por módulos

Contras:


  • La necesidad de un diseño de dependencia más completo: por ejemplo, se debe seguir un cierto orden de inicialización del módulo
  • La complejidad de la gestión de dependencias, especialmente cuando hay muchos
  • Deterioro en la comprensibilidad del código del módulo: escribir código del módulo cuando una dependencia proviene del exterior es más difícil porque no podemos ver directamente esta dependencia.

Resumen:


La inyección de dependencia aumenta la complejidad y el tamaño de la aplicación, pero a cambio permite la reutilización y facilita las pruebas. El desarrollador debe decidir qué es más importante para él en un caso particular: la simplicidad de una dependencia difícil o las posibilidades más amplias de introducir una dependencia.

Servicio de localización


La idea es tener un registro de dependencia que actúe como intermediario al cargar una dependencia con cualquier módulo. En lugar del enlace rígido, el módulo solicita dependencias del localizador de servicios. Obviamente, los módulos tienen una nueva dependencia: el localizador de servicios en sí. Un ejemplo de un localizador de servicios es el sistema de módulos Node.js: los módulos solicitan una dependencia utilizando require (). En el siguiente ejemplo, crearemos un localizador de servicios, registraremos instancias de bases de datos y nuestro módulo en él.

 // serviceLocator.js const dependencies = {}; const factories = {}; const serviceLocator = {}; serviceLocator.register = (name, instance) => { //[2] dependencies[name] = instance; }; serviceLocator.factory = (name, factory) => { //[1] factories[name] = factory; }; serviceLocator.get = (name) => { //[3] if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && factory(serviceLocator); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } return dependencies[name]; }; 

Módulo externo:

 const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule'); 

Nuestro modulo:
 // ourModule.js module.exports = (serviceLocator) => { const db = serviceLocator.get('db'); const someValue = serviceLocator.get('someParameter'); const ourModule = {}; //  ,   ... return ourModule; }; 

Cabe señalar que el localizador de servicios almacena fábricas de servicios en lugar de instancias, y eso tiene sentido. Obtuvimos los beneficios de la inicialización diferida, y ahora no tenemos que preocuparnos por el orden de inicialización de los módulos: todos los módulos se inicializarán cuando sea necesario. Además, tuvimos la oportunidad de almacenar parámetros en el localizador de servicios (ver "algún parámetro").

Pros:


  • Facilidad de redacción de pruebas unitarias
  • Reutilizar un módulo es más fácil que con una adicción difícil
  • Compromiso reducido, mayor conectividad en comparación con la adicción dura
  • Cambiar la responsabilidad de crear dependencias a un nivel superior
  • No es necesario seguir el orden de inicialización del módulo

Contras:


  • Reutilizar un módulo es más difícil que implementar una dependencia (debido a la dependencia adicional del localizador de servicios)
  • Legibilidad: es aún más difícil entender qué hace la dependencia requerida por el localizador de servicios
  • Mayor compromiso en comparación con la inyección de dependencia

Resumen


En general, un localizador de servicios es similar a la inyección de dependencia, de alguna manera es más fácil (no hay orden de inicialización), en algunos casos es más difícil (menos que la posibilidad de reutilizar el código).

Contenedores de dependencia integrados (contenedor DI)


El localizador de servicios tiene un inconveniente debido a que rara vez se aplica en la práctica: la dependencia de los módulos del localizador en sí. Los contenedores de dependencia integrados (contenedores DI) no tienen este inconveniente. De hecho, este es el mismo localizador de servicios con una función adicional que determina las dependencias del módulo antes de crear su instancia. Puede determinar las dependencias del módulo analizando y extrayendo argumentos del constructor del módulo (en JavaScript, puede convertir un enlace a una función en una cadena usando toString ()). Este método es adecuado si el desarrollo va exclusivamente para el servidor. Si se escribe el código del cliente, a menudo se minimiza y no tendrá sentido extraer los nombres de los argumentos. En este caso, la lista de dependencias se puede pasar como una matriz de cadenas (en Angular.js, en función del uso de contenedores DI, se utiliza este enfoque). Implementamos el contenedor DI utilizando el análisis de argumentos de constructor:

 const fnArgs = require('parse-fn-args'); module.exports = function() { const dependencies = {}; const factories = {}; const diContainer = {}; diContainer.factory = (name, factory) => { factories[name] = factory; }; diContainer.register = (name, dep) => { dependencies[name] = dep; }; diContainer.get = (name) => { if(!dependencies[name]) { const factory = factories[name]; dependencies[name] = factory && diContainer.inject(factory); if(!dependencies[name]) { throw new Error('Cannot find module: ' + name); } } diContainer.inject = (factory) => { const args = fnArgs(factory) .map(dependency => diContainer.get(dependency)); return factory.apply(null, args); } return dependencies[name]; }; 

En comparación con el localizador de servicios, se ha agregado el método de inyección, que determina las dependencias del módulo antes de crear su instancia. El código del módulo externo no ha cambiado mucho:

 const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule'); 

Nuestro módulo se ve exactamente igual que con una simple inyección de dependencia:

 // ourModule.js module.exports = (db) => { //       ... }; 

Ahora se puede llamar a nuestro módulo con la ayuda de un contenedor DI y pasarle las instancias de dependencia necesarias directamente, usando una simple inyección de dependencia.

Pros:


  • Facilidad de redacción de pruebas unitarias
  • Fácil reutilización de módulos.
  • Compromiso reducido, mayor conectividad de módulos (especialmente en comparación con un localizador de servicios)
  • Cambiar la responsabilidad de crear dependencias a un nivel superior
  • No es necesario realizar un seguimiento de la inicialización del módulo

El mayor inconveniente:


  • Complicación significativa de la lógica de enlace del módulo

Resumen


Este enfoque es más difícil de entender y contiene un poco más de código, pero vale la pena el tiempo dedicado a él debido a su poder y elegancia. En proyectos pequeños, este enfoque puede ser redundante, pero debe considerarse si se está diseñando una aplicación grande.

Conclusión


Se consideraron los enfoques básicos para el enlace de módulos en Node.js. Como suele suceder, la "bala de plata" no existe, pero el desarrollador debe conocer las posibles alternativas y elegir la solución más adecuada para cada caso específico.

El artículo se basa en un capítulo del libro Node.js Design Patterns publicado en 2017. Desafortunadamente, muchas cosas en el libro ya están desactualizadas, por lo que no puedo recomendar al 100% leerlo, pero algunas cosas siguen siendo relevantes hoy.

Source: https://habr.com/ru/post/438546/


All Articles