Inyección de dependencias, JavaScript y módulos ES6

Otra implementación de Dependency Injection en JavaScript es con módulos ES6, con la capacidad de usar el mismo código en un navegador y en nodejs y no usar transpilers.


imagen


Debajo del corte está mi opinión sobre DI, su lugar en las aplicaciones web modernas, la implementación fundamental de un contenedor DI que puede crear objetos tanto en el frente como en la parte posterior, así como una explicación de lo que Michael Jackson tiene que ver con él.


Les pido encarecidamente a quienes consideran trivial en el artículo que no se violen y no lean hasta el final, para que luego, cuando estén decepcionados, no pongan un "menos". No estoy en contra de los "inconvenientes", pero solo si el signo negativo va acompañado de un comentario, qué exactamente en la publicación causó una reacción negativa. Este es un artículo técnico, así que trate de ser condescendiente con el estilo de presentación y critique con precisión el componente técnico de lo anterior. Gracias


Objetos en la aplicación


Realmente respeto la programación funcional, pero dediqué la mayor parte de mi actividad profesional a crear aplicaciones consistentes en objetos. JavaScript me impresiona con el hecho de que las funciones en él también son objetos. Al crear aplicaciones, pienso en objetos, esta es mi deformación profesional.


Según la duración, los objetos en la aplicación se pueden dividir en las siguientes categorías:


  • permanente : surgen en alguna etapa de la solicitud y se destruyen solo cuando se completa la solicitud;
  • temporal : surgen cuando es necesario realizar alguna operación y se destruyen cuando se completa esta operación;

En este sentido, en la programación existen patrones de diseño tales como:



Es decir, desde mi punto de vista, la aplicación consiste en solitarios existentes permanentemente que realizan las operaciones requeridas ellos mismos o generan objetos temporales para ejecutarlos.


Contenedor de objetos


La inyección de dependencia es un enfoque que facilita la creación de objetos en una aplicación. Es decir, en la aplicación hay un objeto especial que "sabe" cómo crear todos los demás objetos. Tal objeto se llama un Contenedor de Objetos (a veces un Administrador de Objetos).


El contenedor de objetos no es un objeto divino , porque su tarea es solo crear objetos significativos de la aplicación y proporcionarles acceso a otros objetos. La gran mayoría de los objetos de aplicación, generados por el Contenedor y ubicados en él, no tienen idea sobre el Contenedor en sí. Se pueden colocar en cualquier otro entorno, con las dependencias necesarias, y también funcionarán notablemente bien allí (los evaluadores saben a qué me refiero).


Lugar de implementación


En general, hay dos formas de inyectar dependencias en un objeto:


  • a través del constructor;
  • a través de una propiedad (o su accesor);

Básicamente utilicé el primer enfoque, así que continuaré la descripción con el punto de vista de la inyección de dependencia a través del constructor.


Digamos que tenemos una aplicación que consta de tres objetos:


imagen


En PHP (este lenguaje con tradiciones DI de larga data, actualmente tengo equipaje activo, pasaré a JS un poco más tarde) una situación similar podría reflejarse de esta manera:


class Config { public function __construct() { } } class Service { private $config; public function __construct(Config $config) { $this->config = $config; } } class Application { private $config; private $service; public function __construct(Config $config, Service $service) { $this->config = $config; $this->service = $service; } } 

Esta información debería ser suficiente para que un contenedor DI (por ejemplo, liga / contenedor ), si se configura adecuadamente, puede, previa solicitud, crear un objeto Application , crear sus dependencias Service y Config y pasarlos parámetros al constructor del objeto Application .


Identificadores de dependencia


¿Cómo entiende el Contenedor de objetos que el constructor del objeto Application requiere dos objetos Config y Service ? Analizando el objeto a través de la API de Reflection ( Java , PHP ) o analizando el código del objeto directamente (anotaciones de código). Es decir, en el caso general, podemos determinar los nombres de las variables que el constructor de objetos espera ver en la entrada, y si el lenguaje es tipificable, también podemos obtener los tipos de estas variables.


Por lo tanto, como identificadores de objetos, el Contenedor puede operar con los nombres de los parámetros de entrada del constructor o con los tipos de parámetros de entrada.


Crear objetos


El programador puede crear explícitamente el objeto y colocarlo en el Contenedor bajo el identificador correspondiente (por ejemplo, "configuración")


 /** @var \League\Container\Container $container */ $container->add("configuration", $config); 

y puede ser creado por el Contenedor de acuerdo con ciertas reglas específicas. Estas reglas, en general, se reducen a hacer coincidir el identificador del objeto con su código. Las reglas se pueden establecer explícitamente (mapeo en forma de código, XML, JSON, ...)


 [ ["object_id_1", "/path/to/source1.php"], ["object_id_2", "/path/to/source2.php"], ... ] 

o en forma de algún algoritmo:


 public function getSource($id) {. return "/path/to/source/${id}.php"; } 

En PHP, las reglas para hacer coincidir un nombre de clase con un archivo con su código fuente están estandarizadas ( PSR-4 ); en Java, la coincidencia se realiza en el nivel de configuración JVM ( cargador de clases ). Si el Contenedor proporciona una búsqueda automática de fuentes al crear objetos, entonces los nombres de clase son identificadores suficientemente buenos para los objetos en dicho Contenedor.


Espacios de nombres


Por lo general, en un proyecto, además de su propio código, también se utilizan módulos de terceros. Con el advenimiento de los administradores de dependencias (maven, composer, npm), el uso de módulos se ha simplificado enormemente, y el número de módulos en proyectos ha aumentado considerablemente. Los espacios de nombres permiten que existan elementos de código del mismo nombre en un solo proyecto desde varios módulos (clases, funciones, constantes).


Hay idiomas en los que el espacio de nombres está integrado inicialmente (Java):


 package vendor.project.module.folder; 

Hay idiomas en los que se agregó el espacio de nombres durante el desarrollo del lenguaje (PHP):


 namespace Vendor\Project\Module\Folder; 

Una buena implementación del espacio de nombres le permite abordar inequívocamente cualquier elemento del código:


 \Doctrine\Common\Annotations\Annotation\Attribute::$name 

El espacio de nombres resuelve el problema de organizar muchos elementos de software en un proyecto, y la estructura de archivos resuelve el problema de organizar archivos en disco. Por lo tanto, no solo hay mucho en común entre ellos, y a veces mucho, en Java, por ejemplo, una clase pública en el espacio de nombres debe estar unida a un archivo con el código de esta clase.


Por lo tanto, usar el identificador de una clase de objeto en el espacio de nombres del proyecto como identificadores de objeto en el Contenedor es una buena idea y puede servir como base para crear reglas para la detección automática de códigos fuente al crear el objeto deseado.


 $container->add(\Vendor\Project\Module\ObjectType::class, $obj); 

Inicio de código


En PHP composer espacio de nombres composer módulo se asigna al sistema de archivos dentro del módulo en el descriptor del módulo composer.json :


 "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" } } 

La comunidad JS podría hacer una asignación similar en package.json si hubiera espacios de nombres en JS.


Identificadores de dependencia JS


Arriba, indiqué que el Contenedor puede usar los nombres de los parámetros de entrada del constructor o los tipos de parámetros de entrada como identificadores. El problema es que:


  1. JS es un lenguaje con tipeo dinámico y no permite especificar tipos al declarar una función.
  2. JS usa minificadores que pueden cambiar el nombre de los parámetros de entrada.

Los desarrolladores del contenedor awilix DI sugieren usar el objeto como el único parámetro de entrada para el constructor, y las propiedades de este objeto como dependencias:


 class UserController { constructor(opts) { this.userService = opts.userService } } 

El identificador de propiedad del objeto en JS puede consistir en caracteres alfanuméricos, "_" y "$", y puede no comenzar con un dígito.


Como necesitaremos asignar identificadores de dependencia a la ruta de acceso a sus fuentes en el sistema de archivos para la carga automática, es mejor abandonar el uso de "$" y usar la experiencia de PHP. Antes de que apareciera el operador de namespace en algunos marcos (por ejemplo, en Zend 1), se utilizaron los siguientes nombres para las clases:


 class Zend_Config_Writer_Json {...} 

Por lo tanto, podríamos reflejar nuestra aplicación de tres objetos ( Application , Config , Service ) en JS algo como esto:


 class Vendor_Project_Config { constructor() { } } class Vendor_Project_Service { constructor({Vendor_Project_Config}) { this.config = Vendor_Project_Config; } } class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

Si publicamos el código de cada clase:


 export default class Vendor_Project_Application { constructor({Vendor_Project_Config, Vendor_Project_Service}) { this.config = Vendor_Project_Config; this.service = Vendor_Project_Service; } } 

en su archivo dentro de nuestro módulo de proyecto:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

Luego podemos conectar el directorio raíz del módulo con el "espacio de nombres" raíz del módulo en la configuración del Contenedor:


 const ns = "Vendor_Project"; const path = path.join(module_root, "src"); container.addSourceMapping(ns, path); 

y luego, a partir de esta información, construya la ruta a las fuentes correspondientes ( ${module_root}/src/Config.js ) en función del identificador de dependencia ( ${module_root}/src/Config.js ).


Módulos ES6


ES6 ofrece un diseño general para cargar módulos ES6:


 import { something } from 'path/to/source/with/something'; 

Como necesitamos adjuntar un objeto (clase) a un archivo, tiene sentido en la fuente exportar esta clase de manera predeterminada:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

En principio, es posible no escribir un nombre tan largo para la clase, solo Something funcionará también, pero en Zend 1 escribieron y no se rompieron, y la unicidad del nombre de la clase dentro del proyecto afecta positivamente tanto las capacidades del IDE (autocompletar y las indicaciones contextuales), por lo que y al depurar:


imagen


Importar una clase y crear un objeto en este caso se ve así:


 import Something from 'path/to/source/with/something'; const something = new Something(); 

Importación frontal y posterior


Importar funciona tanto en el navegador como en nodejs, pero hay matices. Por ejemplo, el navegador no comprende la importación de módulos nodejs:


 import path from "path"; 

Recibimos un error en el navegador:


 Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../". 

Es decir, si queremos que nuestro código funcione tanto en el navegador como en nodejs, no podemos usar construcciones que el navegador o nodejs no entiendan. Me concentro específicamente en esto, porque tal conclusión es demasiado natural para pensarlo. Cómo respirar


El lugar de DI en las aplicaciones web modernas


Esta es puramente mi opinión personal, debido a mi experiencia personal, como todo lo demás en esta publicación.


En las aplicaciones web, JS prácticamente ocupa su lugar en la parte frontal, en el navegador, casi sin alternativa. En el lado del servidor, Java, PHP, .Net, Ruby, Python, densamente excavados ... Pero con el advenimiento de nodejs, JavaScript también penetró en el servidor. Y las tecnologías utilizadas en otros idiomas, incluido DI, comenzaron a penetrar en JS del lado del servidor.


El desarrollo de JavaScript se debe al comportamiento asíncrono del código en el navegador. La asincronía no es una característica excepcional de JS, sino más bien innata. Ahora, la presencia de JS tanto en el servidor como en el frente no sorprende a nadie, sino que alienta el uso de los mismos enfoques en ambos extremos de la aplicación web. Y el mismo código. Por supuesto, el anverso y el reverso son demasiado diferentes en esencia y en las tareas a resolver para usar el mismo código tanto allí como allá. Pero podemos suponer que en una aplicación más o menos compleja habrá navegador, servidor y código general.


DI ya está en uso en la parte delantera, en RequireJS :


 define( ["./config", "./service"], function App(Config, Service) {} ); 

Es cierto que aquí los identificadores de dependencias se escriben explícita e inmediatamente en forma de enlaces a las fuentes (puede configurar la asignación de identificadores en la configuración del cargador de arranque).


En las aplicaciones web modernas, la DI existe no solo en el lado del servidor, sino también en el navegador.


¿Qué tiene que ver Michael Jackson con eso?


Cuando habilita el soporte del módulo ES en nodejs (flag --experimental-modules ), el motor identifica el contenido de los archivos con la *.mjs como módulos EcmaScript (a diferencia de los módulos comunes con la *.cjs ).


Este enfoque a veces se llama la " Solución Michael Jackson ", y los guiones se llaman Michael Jackson Scripts ( *.mjs ).


Estoy de acuerdo en que la intriga del KDPV se resolvió, pero ... el camon de los muchachos , Michael Jackson ...


Otra implementación más de DI


Bueno, como se esperaba, el tuyo la bici Módulo DI - @ teqfw / di


Esta no es una solución lista para luchar, sino una implementación fundamental. Todas las dependencias deben ser módulos ES y utilizar funciones comunes para el navegador y nodejs.


Para resolver dependencias, el módulo utiliza el enfoque awilix :


 constructor(spec) { /** @type {Vendor_Module_Config} */ const _config = spec.Vendor_Module_Config; /** @type {Vendor_Module_Service} */ const _service = spec.Vendor_Module_Service; } 

Para ejecutar el ejemplo anterior:


 import Container from "./src/Container.mjs"; const container = new Container(); container.addSourceMapping("Vendor_Module", "../example"); container.get("Vendor_Module_App") .then((app) => { app.run(); }); 

en el servidor:


 $ node --experimental-modules main.mjs 

Para ejecutar el ejemplo frontal ( example.html ):


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>DI in Browser</title> <script type="module" src="./main.mjs"></script> </head> <body> <p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p> <p>Open browser console to see output.</p> </body> </html> 

debe colocar el módulo en el servidor y abrir la página example.html en el navegador (o usar las capacidades del IDE). Si abre example.html directamente, entonces el error en Chrom es:


 Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https. 

Si todo salió bien, entonces en la consola (navegador o nodejs) habrá algo como esto:


 Create object with ID 'Vendor_Module_App'. Create object with ID 'Vendor_Module_Config'. There is no dependency with id 'Vendor_Module_Config' yet. 'Vendor_Module_Config' instance is created. Create object with ID 'Vendor_Module_Service'. There is no dependency with id 'Vendor_Module_Service' yet. 'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]). 'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]). Application 'Vendor_Module_Config' is running. 

Resumen


AMD, CommonJS, UMD?


ESM !

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


All Articles