De nombreux développeurs de Node.js utilisent des dépendances matérielles de module (exclusivement) en utilisant require () pour lier des modules, mais il existe d'autres approches avec leurs avantages et leurs inconvénients. J'en parlerai dans cet article. Quatre approches seront envisagées:
- Dépendances matérielles (require ())
- Injection de dépendance
- Localisateur de services
- Conteneurs de dépendance intégrés (conteneur DI)
Un peu sur les modules
Les modules et l'architecture modulaire sont à la base de Node.js. Les modules fournissent l'encapsulation (masquant les détails de l'implémentation et ouvrant uniquement l'interface avec module.exports), la réutilisation du code, la division du code logique en fichiers. Presque toutes les applications Node.js se composent de nombreux modules qui doivent en quelque sorte interagir. Si vous liez de manière incorrecte les modules ou laissez même l'interaction des modules dériver, vous pouvez très rapidement constater que l'application commence à "s'effondrer": les modifications du code à un endroit entraînent une panne dans un autre et les tests unitaires deviennent tout simplement impossibles. Idéalement, les modules devraient avoir une
connectivité élevée, mais un faible
couplage .
Dépendance dure
Une forte dépendance d'un module à un autre se produit lorsque require () est utilisé. Il s'agit d'une approche efficace, simple et courante. Par exemple, nous voulons simplement connecter le module chargé d'interagir avec la base de données:
Avantages:
- Simplicité
- Organisation visuelle des modules
- Débogage facile
Inconvénients:
- Difficulté à réutiliser le module (par exemple, si nous voulons utiliser notre module à plusieurs reprises, mais avec une instance différente de la base de données)
- Difficulté pour les tests unitaires (vous devez créer une instance de base de données factice et la transmettre en quelque sorte au module)
Résumé:
L'approche est bonne pour les petites applications ou prototypes, ainsi que pour la connexion de modules sans état: usines, concepteurs et jeux de fonctionnalités.
Injection de dépendance
L'idée principale de l'injection de dépendances est de transférer les dépendances d'un composant externe au module. Ainsi, la forte dépendance dans le module est supprimée et il devient possible de le réutiliser dans différents contextes (par exemple, avec différentes instances de base de données).
L'injection de dépendances peut être implémentée en passant la dépendance dans l'argument constructeur ou en définissant les propriétés du module, mais en pratique, il est préférable d'utiliser la première méthode. Appliquons concrètement l'implémentation des dépendances en créant une instance de la base de données à l'aide de la fabrique et en la passant à notre module:
Module externe:
const dbFactory = require('db'); const OurModule = require('./ourModule.js'); const dbInstance = dbFactory.createInstance('instance1'); const ourModule = OurModule(dbInstance);
Maintenant, nous pouvons non seulement réutiliser notre module, mais également écrire facilement un test unitaire pour lui: créez simplement un objet factice pour l'instance de base de données et passez-le au module.
Avantages:
- Facilité d'écriture des tests unitaires
- Augmenter la réutilisabilité des modules
- Engagement réduit, connectivité accrue
- Déplacer la responsabilité de la création de dépendances à un niveau supérieur - cela améliore souvent la lisibilité du programme, car les dépendances importantes sont collectées en un seul endroit et ne sont pas réparties par modules
Inconvénients:
- La nécessité d'une conception de dépendance plus approfondie: par exemple, un certain ordre d'initialisation du module doit être suivi
- La complexité de la gestion des dépendances, surtout lorsqu'il existe de nombreux
- Détérioration de la compréhensibilité du code du module: écrire du code de module lorsqu'une dépendance vient de l'extérieur est plus difficile car on ne peut pas regarder directement cette dépendance.
Résumé:
L'injection de dépendances augmente la complexité et la taille de l'application, mais en retour permet la réutilisation et facilite les tests. Le développeur doit décider de ce qui est le plus important pour lui dans un cas particulier - la simplicité d'une forte dépendance ou les possibilités plus larges d'introduire une dépendance.
Localisateur de services
L'idée est d'avoir un registre de dépendances qui sert d'intermédiaire lors du chargement d'une dépendance avec n'importe quel module. Au lieu d'une liaison matérielle, les dépendances sont demandées par le module au localisateur de services. De toute évidence, les modules ont une nouvelle dépendance - le localisateur de services lui-même. Un exemple de localisateur de services est le système de modules Node.js: les modules demandent une dépendance en utilisant require (). Dans l'exemple suivant, nous allons créer un localisateur de service, enregistrer les instances de base de données et notre module dedans.
Module externe:
const serviceLocator = require('./serviceLocator.js')(); serviceLocator.register('someParameter', 'someValue'); serviceLocator.factory('db', require('db')); serviceLocator.factory('ourModule', require('ourModule')); const ourModule = serviceLocator.get('ourModule');
Notre module:
Il convient de noter que le localisateur de services stocke des usines de service au lieu d'instances, ce qui est logique. Nous avons obtenu les avantages de l'initialisation paresseuse, et maintenant nous n'avons plus à nous soucier de l'ordre d'initialisation des modules - tous les modules seront initialisés quand cela sera nécessaire. De plus, nous avons eu la possibilité de stocker des paramètres dans le localisateur de service (voir "someParameter").
Avantages:
- Facilité d'écriture des tests unitaires
- Réutiliser un module est plus facile qu'avec une forte dépendance
- Engagement réduit, connectivité accrue par rapport à la dépendance dure
- Faire passer la responsabilité de la création de dépendances à un niveau supérieur
- Pas besoin de suivre l'ordre d'initialisation du module
Inconvénients:
- La réutilisation d'un module est plus difficile que l'implémentation d'une dépendance (en raison de la dépendance supplémentaire du localisateur de service)
- Lisibilité: il est encore plus difficile de comprendre ce que fait la dépendance requise par le localisateur de services
- Engagement accru par rapport à l'injection de dépendance
Résumé
En général, un localisateur de services est similaire à l'injection de dépendances, à certains égards, il est plus facile (il n'y a pas d'ordre d'initialisation), dans certains cas, il est plus difficile (moins que la possibilité de réutiliser du code).
Conteneurs de dépendance intégrés (conteneur DI)
Le localisateur de services présente un inconvénient en raison duquel il est rarement appliqué dans la pratique - la dépendance des modules sur le localisateur lui-même. Les conteneurs de dépendance intégrés (conteneurs DI) n'ont pas cet inconvénient. En fait, il s'agit du même localisateur de services avec une fonction supplémentaire qui détermine les dépendances du module avant de créer son instance. Vous pouvez déterminer les dépendances du module en analysant et en extrayant les arguments du constructeur du module (en JavaScript, vous pouvez convertir un lien vers une fonction en chaîne à l'aide de toString ()). Cette méthode convient si le développement se fait uniquement pour le serveur. Si du code client est écrit, il est souvent minifié et il sera inutile d'extraire les noms des arguments. Dans ce cas, la liste des dépendances peut être passée sous forme de tableau de chaînes (dans Angular.js, basée sur l'utilisation de conteneurs DI, cette approche est utilisée). Nous implémentons le conteneur DI en utilisant l'analyse des arguments du constructeur:
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]; };
Par rapport au localisateur de services, la méthode inject a été ajoutée, ce qui détermine les dépendances du module avant de créer son instance. Le code du module externe n'a pas beaucoup changé:
const diContainer = require('./diContainer.js')(); diContainer.register('someParameter', 'someValue'); diContainer.factory('db', require('db')); diContainer.factory('ourModule', require('ourModule')); const ourModule = diContainer.get('ourModule');
Notre module ressemble exactement à une injection de dépendance simple:
Maintenant, notre module peut être appelé à la fois à l'aide d'un conteneur DI et en lui passant directement les instances de dépendance nécessaires, en utilisant une simple injection de dépendance.
Avantages:
- Facilité d'écriture des tests unitaires
- Réutilisation facile des modules
- Engagement réduit, connectivité accrue des modules (en particulier par rapport à un localisateur de services)
- Faire passer la responsabilité de la création de dépendances à un niveau supérieur
- Pas besoin de suivre l'initialisation du module
Le plus gros inconvénient:
- Complication importante de la logique de liaison des modules
Résumé
Cette approche est plus difficile à comprendre et contient un peu plus de code, mais elle en vaut la peine en raison de sa puissance et de son élégance. Dans les petits projets, cette approche peut être redondante, mais doit être envisagée si une grande application est en cours de conception.
Conclusion
Les approches de base de la liaison de modules dans Node.js. ont été considérées. Comme cela arrive généralement, la «solution miracle» n'existe pas, mais le développeur doit être conscient des alternatives possibles et choisir la solution la plus appropriée pour chaque cas spécifique.
L'article est basé sur un chapitre du livre
Node.js Design Patterns publié en 2017. Malheureusement, beaucoup de choses dans le livre sont déjà dépassées, donc je ne peux pas recommander à 100% de le lire, mais certaines choses sont toujours d'actualité aujourd'hui.