Modules d'injection de dépendances, JavaScript et ES6

Une autre implémentation de Dependency Injection en JavaScript est avec les modules ES6, avec la possibilité d'utiliser le même code dans un navigateur et dans nodejs et de ne pas utiliser de transpilers.


image


Sous la coupe se trouve mon point de vue sur DI, sa place dans les applications Web modernes, l'implémentation fondamentale d'un conteneur DI qui peut créer des objets à l'avant et à l'arrière, ainsi qu'une explication de ce que Michael Jackson a à voir avec cela.


Je demande très fortement à ceux qui trouvent trivial dans l'article de ne pas se violer et de ne pas lire jusqu'au bout, afin que plus tard, s'ils sont déçus, ne mettent pas de «moins». Je ne suis pas contre les «moins» - mais seulement si le moins est accompagné d'un commentaire, ce qui exactement dans la publication a provoqué une réaction négative. Il s'agit d'un article technique, essayez donc d'être condescendant envers le style de présentation et critiquez précisément la composante technique de ce qui précède. Je vous remercie


Objets dans l'application


Je respecte vraiment la programmation fonctionnelle, mais j'ai consacré l'essentiel de mon activité professionnelle à la création d'applications constituées d'objets. JavaScript m'impressionne du fait que les fonctions qu'il contient sont également des objets. Lors de la création d'applications, je pense aux objets, c'est ma déformation professionnelle.


Selon la durée de vie, les objets de l'application peuvent être répartis dans les catégories suivantes:


  • permanent - survient à un certain stade de la demande et n'est détruit que lorsque la demande est terminée;
  • temporaire - survient lorsqu'il est nécessaire d'effectuer une opération et est détruit lorsque cette opération est terminée;

À cet égard, dans la programmation, il existe des modèles de conception tels que:



Autrement dit, de mon point de vue, l'application consiste en des solitaires existants en permanence qui effectuent eux-mêmes les opérations requises ou génèrent des objets temporaires pour les exécuter.


Conteneur d'objets


L'injection de dépendances est une approche qui facilite la création d'objets dans une application. Autrement dit, dans l'application, il y a un objet spécial qui «sait» comment créer tous les autres objets. Un tel objet est appelé un conteneur d'objets (parfois un gestionnaire d'objets).


Le conteneur d'objets n'est pas un objet divin , car sa tâche est uniquement de créer des objets significatifs de l'application et de leur donner accès à d'autres objets. La grande majorité des objets d'application, générés par le conteneur et situés dans celui-ci, n'ont aucune idée du conteneur lui-même. Ils peuvent être placés dans n'importe quel autre environnement, pourvu des dépendances nécessaires, et ils fonctionneront également à merveille là-bas (les testeurs savent ce que je veux dire).


Lieu d'exécution


Dans l'ensemble, il existe deux façons d'injecter des dépendances dans un objet:


  • par le constructeur;
  • par une propriété (ou son accesseur);

J'ai essentiellement utilisé la première approche, donc je vais continuer la description avec le point de vue de l'injection de dépendance via le constructeur.


Disons que nous avons une application composée de trois objets:


image


En PHP (ce langage avec des traditions DI de longue date, j'ai actuellement des bagages actifs, je vais passer à JS un peu plus tard) une situation similaire pourrait se refléter de cette manière:


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; } } 

Ces informations doivent être suffisantes pour qu'un conteneur DI (par exemple, ligue / conteneur ), s'il est configuré de manière appropriée, puisse, sur demande pour créer un objet Application , créer ses dépendances Service et Config et leur transmettre des paramètres au constructeur de l'objet Application .


Identifiants de dépendance


Comment le conteneur d'objets comprend-il que le constructeur de l'objet Application nécessite deux objets Config et Service ? En analysant l'objet via l'API Reflection ( Java , PHP ) ou en analysant directement le code objet (annotations de code). Autrement dit, dans le cas général, nous pouvons déterminer les noms des variables que le constructeur d'objet s'attend à voir en entrée, et si le langage est typable, nous pouvons également obtenir les types de ces variables.


Ainsi, en tant qu'identificateurs d'objets, le conteneur peut fonctionner avec les noms des paramètres d'entrée du constructeur ou les types de paramètres d'entrée.


Créer des objets


L'objet peut être créé explicitement par le programmeur et placé dans le conteneur sous l'identifiant correspondant (par exemple, "configuration")


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

et peut être créé par le conteneur selon certaines règles spécifiques. Ces règles, dans l'ensemble, se résument à faire correspondre l'identifiant de l'objet à son code. Les règles peuvent être définies explicitement (mappage sous forme de code, XML, JSON, ...)


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

ou sous la forme d'un algorithme:


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

En PHP, les règles de correspondance d'un nom de classe à un fichier avec son code source sont standardisées ( PSR-4 ); en Java, la correspondance se fait au niveau de la configuration JVM ( chargeur de classe ). Si le conteneur fournit une recherche automatique des sources lors de la création d'objets, les noms de classe sont des identifiants suffisamment bons pour les objets dans un tel conteneur.


Espaces de noms


Habituellement, dans un projet, en plus de son propre code, des modules tiers sont également utilisés. Avec l'avènement des gestionnaires de dépendances (maven, composer, npm), l'utilisation des modules a été considérablement simplifiée et le nombre de modules dans les projets a considérablement augmenté. Les espaces de noms permettent à des éléments de code du même nom d'exister dans un même projet à partir de différents modules (classes, fonctions, constantes).


Il existe des langages dans lesquels l'espace de noms est intégré initialement (Java):


 package vendor.project.module.folder; 

Il existe des langages dans lesquels l'espace de noms a été ajouté lors du développement du langage (PHP):


 namespace Vendor\Project\Module\Folder; 

Une bonne implémentation d'espace de noms vous permet d'adresser sans ambiguïté n'importe quel élément du code:


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

L'espace de noms résout le problème de l'organisation de nombreux éléments logiciels dans un projet, et la structure de fichiers résout le problème de l'organisation des fichiers sur le disque. Par conséquent, il n'y a pas seulement beaucoup de choses en commun entre eux, et parfois beaucoup - en Java, par exemple, une classe publique dans l'espace de noms doit être uniquement attachée à un fichier avec le code de cette classe.


Ainsi, l'utilisation de l'identifiant d'une classe d'objet dans l'espace de noms du projet comme identifiants d'objet dans le conteneur est une bonne idée et peut servir de base pour créer des règles pour la détection automatique des codes source lors de la création de l'objet souhaité.


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

Démarrage du code


Dans PHP composer espace de noms composer module est mappé sur le système de fichiers à l'intérieur du module dans le descripteur du module composer.json :


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

La communauté JS pourrait effectuer un mappage similaire dans package.json s'il y avait des espaces de noms dans JS.


Identifiants de dépendance JS


Ci-dessus, j'ai indiqué que le conteneur peut utiliser les noms des paramètres d'entrée du constructeur ou les types de paramètres d'entrée comme identificateurs. Le problème est que:


  1. JS est un langage à typage dynamique et ne permet pas de spécifier des types lors de la déclaration d'une fonction.
  2. JS utilise des minificateurs qui peuvent renommer les paramètres d'entrée.

Les développeurs du conteneur awilix DI suggèrent d'utiliser l'objet comme seul paramètre d'entrée pour le constructeur, et les propriétés de cet objet comme dépendances:


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

L'identificateur de propriété d'objet dans JS peut être composé de caractères alphanumériques, "_" et "$", et ne peut pas commencer par un chiffre.


Étant donné que nous devrons mapper les identificateurs de dépendance sur le chemin d'accès à leurs sources dans le système de fichiers pour le chargement automatique, il est préférable d'abandonner l'utilisation de "$" et d'utiliser l'expérience de PHP. Avant que l'opérateur d' namespace n'apparaisse dans certains frameworks (par exemple, dans Zend 1), les noms suivants étaient utilisés pour les classes:


 class Zend_Config_Writer_Json {...} 

Ainsi, nous pourrions refléter notre application de trois objets ( Application , Config , Service ) sur JS quelque chose comme ceci:


 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 nous affichons le code de chaque classe:


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

dans votre dossier à l'intérieur de notre module projet:


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

Ensuite, nous pouvons connecter le répertoire racine du module avec le "namespace" racine du module dans la configuration du conteneur:


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

puis, à partir de ces informations, construisez le chemin d'accès aux sources correspondantes ( ${module_root}/src/Config.js ) en fonction de l'identificateur de dépendance ( ${module_root}/src/Config.js ).


Modules ES6


ES6 propose une conception générale pour le chargement des modules ES6:


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

Comme nous devons attacher un objet (classe) à un fichier, il est logique dans la source d'exporter cette classe par défaut:


 export default class Vendor_Project_Path_To_Source_With_Something {...} 

En principe, il est possible de ne pas écrire un nom aussi long pour la classe, juste Something fonctionnera aussi, mais dans Zend 1, ils ont écrit et ne se sont pas cassés, et l'unicité du nom de classe dans le projet affecte positivement les capacités de l'IDE (autocomplete et invites contextuelles), donc et lors du débogage:


image


L'importation d'une classe et la création d'un objet dans ce cas ressemble à ceci:


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

Importation avant et arrière


L'importation fonctionne à la fois dans le navigateur et dans nodejs, mais il y a des nuances. Par exemple, le navigateur ne comprend pas l'importation des modules nodejs:


 import path from "path"; 

Nous obtenons une erreur dans le navigateur:


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

Autrement dit, si nous voulons que notre code fonctionne à la fois dans le navigateur et dans nodejs, nous ne pouvons pas utiliser de constructions que le navigateur ou nodejs ne comprend pas. Je me concentre spécifiquement sur cela, car une telle conclusion est trop naturelle pour y penser. Comment respirer.


La place de DI dans les applications web modernes


Ceci est purement mon opinion personnelle, en raison de mon expérience personnelle, comme tout le reste dans cette publication.


Dans les applications Web, JS prend pratiquement sa place à l'avant, dans le navigateur, presque sans alternative. Côté serveur, Java, PHP, .Net, Ruby, python, densément creusé ... Mais avec l'avènement de nodejs, JavaScript a également pénétré le serveur. Et les technologies utilisées dans d'autres langages, y compris DI, ont commencé à pénétrer JS côté serveur.


Le développement de JavaScript est dû au comportement asynchrone du code dans le navigateur. L'asynchronie n'est pas une caractéristique exceptionnelle de JS, mais plutôt innée. Désormais, la présence de JS sur le serveur et sur le devant ne surprend personne, mais encourage plutôt l'utilisation des mêmes approches aux deux extrémités de l'application Web. Et le même code. Bien sûr, l'avant et l'arrière sont trop différents par essence et dans les tâches à résoudre pour utiliser le même code là et là. Mais nous pouvons supposer que dans une application plus ou moins complexe, il y aura un navigateur, un serveur et un code général.


DI est déjà utilisé à l'avant, dans RequireJS :


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

Certes, ici les identifiants des dépendances sont écrits explicitement et immédiatement sous forme de liens vers les sources (vous pouvez configurer le mappage des identifiants dans la configuration du chargeur de démarrage).


Dans les applications Web modernes, DI existe non seulement côté serveur, mais aussi dans le navigateur.


Qu'est-ce que Michael Jackson a à voir avec ça?


Lorsque vous activez la prise en charge du module ES dans nodejs (indicateur --experimental-modules ), le moteur identifie le contenu des fichiers avec l' *.mjs tant que modules EcmaScript (contrairement aux modules communs avec l' *.cjs ).


Cette approche est parfois appelée « Michael Jackson Solution », et les scripts sont appelés Michael Jackson Scripts ( *.mjs ).


Je suis d'accord que l'intrigue avec le KDPV a été résolue, mais ... le camon des gars, Michael Jackson ...


Encore une autre implémentation DI


Eh bien, comme prévu, le vôtre le vélo Module DI - @ teqfw / di


Ce n'est pas une solution prête à combattre, mais plutôt une mise en œuvre fondamentale. Toutes les dépendances doivent être des modules ES et utiliser des fonctionnalités communes pour le navigateur et nodejs.


Pour résoudre les dépendances, le module utilise l'approche awilix :


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

Pour exécuter l'exemple précédent:


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

sur le serveur:


 $ node --experimental-modules main.mjs 

Pour exécuter l'exemple 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> 

vous devez placer le module sur le serveur et ouvrir la page example.html dans le navigateur (ou utiliser les capacités de l'EDI). Si vous ouvrez directement example.html , l'erreur dans Chrom est la suivante:


 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 tout s'est bien passé, alors dans la console (navigateur ou nodejs) il y aura quelque chose comme ça:


 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. 

Résumé


AMD, CommonJS, UMD?


ESM !

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


All Articles