Je ne suis pas un grand fan des grands frameworks comme NestJS ; J'ai toujours aimé la liberté de créer mes logiciels comme je le souhaite, avec la structure que je décide de manière légère. Mais ce que j'ai aimé lors du test de NestJS, c'est l'injection de dépendances.
Dependency Injection (DI) est un modèle de conception qui nous permet de développer du code faiblement couplé en supprimant la responsabilité de créer et de gérer les dépendances de nos classes. Ce modèle est crucial pour écrire des applications maintenables, testables et évolutives. Dans l'écosystème TypeScript, TSyringe se distingue comme un conteneur d'injection de dépendances puissant et léger qui simplifie ce processus.
TSyringe est un conteneur d'injection de dépendances léger pour les applications TypeScript/JavaScript. Maintenu par Microsoft sur leur GitHub (https://github.com/microsoft/tsyringe), il utilise des décorateurs pour effectuer l'injection de constructeur. Ensuite, il utilise un conteneur d'inversion de contrôle pour stocker les dépendances en fonction d'un jeton que vous pouvez échanger contre une instance ou une valeur.
Avant de plonger dans TSyringe, explorons brièvement ce qu'est l'injection de dépendance et pourquoi elle est importante.
L'injection de dépendances est une technique dans laquelle un objet reçoit ses dépendances de sources externes plutôt que de les créer lui-même. Cette approche offre plusieurs avantages :
Tout d'abord, configurons TSyringe dans votre projet TypeScript :
npm install tsyringe reflect-metadata
Dans votre tsconfig.json, assurez-vous d'avoir les options suivantes :
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }
Importez les métadonnées de réflexion au point d'entrée de votre application :
import "reflect-metadata";
Le point d'entrée de votre application est, par exemple, la disposition racine sur Next.js 13 , ou il peut s'agir du fichier principal dans une petite application Express.
Reprenons l'exemple de l'introduction et ajoutons le sucre TSeringue :
Commençons par l'adaptateur.
// @/adapters/userAdapter.ts import { injectable } from "tsyringe" @injectable() class UserAdapter { constructor(...) {...} async fetchByUUID(uuid) {...} }
Remarquez le décorateur @injectable() ? Il s'agit d'indiquer à TSyringe que cette classe peut être injectée au moment de l'exécution.
Mon service utilise donc l'adaptateur que nous venons de créer. Injectons cet adaptateur dans mon service.
// @/core/user/user.service.ts import { injectable, inject } from "tsyringe" ... @injectable() class UserService { constructor(@inject('UserAdapter') private readonly userAdapter: UserAdapter) {} async fetchByUUID(uuid: string) { ... const { data, error } = await this.userAdapter.fetchByUUID(uuid); ... } }
Ici, j'ai également utilisé le décorateur @injectable car le Service va être injecté dans ma classe de commande, mais j'ai également ajouté le décorateur @inject dans les paramètres du constructeur. Ce décorateur indique à TSyringe de donner l'instance ou la valeur qu'elle a pour le jeton UserAdapter pour la propriété userAdapter au moment de l'exécution.
Et enfin, la racine de mon Core : la classe de commande (souvent appelée à tort usecase).
// @/core/user/user.commands.ts import { inject } from "tsyringe" ... @injectable() class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } }
À ce stade, nous avons indiqué à TSyringe ce qui va être injecté et quoi injecter dans le constructeur. Mais nous n'avons toujours pas réalisé nos conteneurs pour stocker les dépendances. Nous pouvons le faire de deux manières :
Nous pouvons créer un fichier avec notre registre d'injection de dépendances :
// @/core/user/user.dependencies.ts import { container } from "tsyringe" ... container.register("UserService", {useClass: UserService}) // associate the UserService with the token "UserService" container.register("UserAdapter", {useClass: UserAdapter}) // associate the UserAdapter with the token "UserAdapter" export { container }
Mais on peut aussi utiliser le décorateur @registry.
// @/core/user/user.commands.ts import { inject, registry, injectable } from "tsyringe" ... @injectable() @registry([ { token: 'UserService', useClass: UserService }, { token: 'UserAdapter', useClass: UserAdapter }, ]) export class UserCommands { constructor(@inject('UserService') private readonly userService: UserService) {} async fetchByUUID(uuid) { ... const { data, error } = this.userService.fetchByUUID(uuid); ... } } container.register("UserCommands", { useClass: UserCommands}) export { container }
Les deux méthodes ont des avantages et des inconvénients, mais en fin de compte, c'est une question de goût.
Maintenant que notre conteneur est rempli de nos dépendances, nous pouvons les obtenir du conteneur selon nos besoins en utilisant la méthode de résolution du conteneur.
import { container, UserCommands } from "@/core/user/user.commands" ... const userCommands = container.resolve("UserCommands") await userCommands.fetchByUUID(uuid) ...
Cet exemple est assez simple car chaque classe ne dépend que d'une autre, mais nos services pourraient dépendre de plusieurs, et l'injection de dépendances aiderait vraiment à garder tout en ordre.
Mais attendez ! Ne me laisse pas comme ça ! Et les tests ?
Nos injections peuvent également nous aider à tester notre code en envoyant des objets fictifs directement dans nos dépendances. Voyons un exemple de code :
import { container, UserCommands } from "@/core/user/user.commands" describe("test ftw", () => { let userAdapterMock: UserAdapterMock let userCommands: UserCommands beforeEach(() => { userAdapterMock = new UserAdapter() container.registerInstance("UserAdapter", userAdapter) userCommands = container.resolve ("UserCommands") }); ... });
Maintenant, le jeton UserAdapter contient une simulation qui sera injectée dans les classes dépendantes.
Utiliser des jetons pour nommer : au lieu d'utiliser des chaînes littérales pour les jetons d'injection, créez des jetons constants :
export const USER_REPOSITORY_TOKEN = Symbol("UserRepository");
Conteneurs étendus : utilisez des conteneurs étendus pour les dépendances étendues aux requêtes dans les applications Web.
N'abusez pas de DI : Tout n'a pas besoin d'être injecté. Utilisez DI pour les préoccupations transversales et les dépendances configurables.
Si vous êtes arrivé jusqu'ici, je tiens à vous remercier d'avoir lu. J'espère que vous avez trouvé cet article instructif. N'oubliez pas de toujours prendre en compte les besoins spécifiques de votre projet lors de la mise en œuvre de l'injection de dépendances et des modèles architecturaux.
Les likes et les commentaires sont les meilleurs moyens de s'améliorer.
Bon codage !
Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.
Copyright© 2022 湘ICP备2022001581号-3