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 quelque chose 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 léger d'injection de dépendances 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 réfléchies au point d'entrée de votre application :
import "reflect-metadata";
Le point d'entrée de votre application est, par exemple, la mise en page racine sur Next.js 13+, ou il peut être le 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() ? C'est pour dire à 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 récupérer 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>("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", userAdapter) userCommands = container.resolve<UserCommands>("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");
Scoped containers: Use scoped containers for request-scoped dependencies in web applications.
Don't overuse DI: Not everything needs to be injected. Use DI for cross-cutting concerns and configurable dependencies.
If you've come this far, I want to say thank you for reading. I hope you found this article instructive. Remember to always consider the specific needs of your project when implementing dependency injection and architectural patterns.
Likes and comment feedback are the best ways to improve.
Happy coding!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!