Passer au contenu

Fournisseurs personnalisés

Dans les chapitres précédents, nous avons abordé divers aspects de l’injection de dépendances (DI) et comment elle est utilisée dans Nest. Un exemple de cela est l’injection de dépendances basée sur le constructeur utilisée pour injecter des instances (souvent des fournisseurs de services) dans des classes. Vous ne serez pas surpris d’apprendre que l’injection de dépendances est intégrée au cœur de Nest de manière fondamentale. Jusqu’à présent, nous n’avons exploré qu’un seul schéma principal. À mesure que votre application devient plus complexe, vous pourriez avoir besoin de profiter de toutes les fonctionnalités du système DI, alors explorons-les plus en détail.

Fondamentaux de DI

L’injection de dépendances est une technique d’inversion de contrôle (IoC) dans laquelle vous déléguez l’instanciation de dépendances à un conteneur IoC (dans notre cas, le système d’exécution NestJS), plutôt que de le faire dans votre propre code de manière impérative. Examinons ce qui se passe dans cet exemple du chapitre des Fournisseurs.

Tout d’abord, nous définissons un fournisseur. Le décorateur @Injectable() marque la classe CatsService comme un fournisseur.

cats.service.ts

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
findAll(): Cat[] {
return this.cats;
}
}

Ensuite, nous demandons à Nest d’injecter le fournisseur dans notre classe de contrôleur.

cats.controller.ts

import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}

Enfin, nous enregistrons le fournisseur auprès du conteneur IoC de Nest.

app.module.ts

import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}

Que se passe-t-il exactement en coulisses pour que cela fonctionne ? Il y a trois étapes clés dans le processus :

  1. Dans cats.service.ts, le décorateur @Injectable() déclare la classe CatsService comme une classe pouvant être gérée par le conteneur IoC de Nest.
  2. Dans cats.controller.ts, CatsController déclare une dépendance au token CatsService avec une injection par constructeur :
constructor(private catsService: CatsService)
  1. Dans app.module.ts, nous associons le token CatsService à la classe CatsService du fichier cats.service.ts. Nous verrons ci-dessous exactement comment cette association (également appelée enregistrement) se produit.

Lorsque le conteneur IoC de Nest instancie un CatsController, il recherche d’abord toutes les dépendances. Lorsqu’il trouve la dépendance CatsService, il effectue une recherche sur le token CatsService, qui renvoie la classe CatsService, selon l’étape d’enregistrement. En supposant une portée SINGLETON (le comportement par défaut), Nest créera alors soit une instance de CatsService, la mettra en cache et la renverra, soit si une instance est déjà mise en cache, renverra l’instance existante.

Cette explication est un peu simplifiée pour illustrer le propos. Un aspect important que nous avons contourné est que le processus d’analyse du code pour les dépendances est très sophistiqué, et se produit lors de l’initialisation de l’application. Une caractéristique clé est que l’analyse des dépendances (ou “création du graphe de dépendance”) est transitive. Dans l’exemple ci-dessus, si le CatsService lui-même avait des dépendances, celles-ci seraient également résolues. Le graphe de dépendance garantit que les dépendances sont résolues dans le bon ordre - essentiellement “de bas en haut”. Ce mécanisme soulage le développeur de la nécessité de gérer des graphes de dépendance aussi complexes.

Fournisseurs standards

Examinons de plus près le décorateur @Module(). Dans app.module, nous déclarons :

@Module({
controllers: [CatsController],
providers: [CatsService],
})

La propriété providers prend un tableau de fournisseurs. Jusqu’à présent, nous avons fourni ces fournisseurs via une liste de noms de classes. En fait, la syntaxe providers: [CatsService] est une abréviation pour la syntaxe plus complète :

providers: [
{
provide: CatsService,
useClass: CatsService,
},
];

Maintenant que nous voyons cette construction explicite, nous pouvons comprendre le processus d’enregistrement. Ici, nous associons clairement le token CatsService à la classe CatsService. La notation abrégée est simplement une commodité pour simplifier le cas d’utilisation le plus courant, où le token est utilisé pour demander une instance d’une classe portant le même nom.

Fournisseurs personnalisés

Que se passe-t-il lorsque vos exigences dépassent celles offertes par les fournisseurs standards ? Voici quelques exemples :

  • Vous souhaitez créer une instance personnalisée au lieu de laisser Nest instancier (ou renvoyer une instance mise en cache) une classe.
  • Vous souhaitez réutiliser une classe existante dans une deuxième dépendance.
  • Vous souhaitez remplacer une classe par une version factice pour les tests.

Nest vous permet de définir des fournisseurs personnalisés pour gérer ces cas. Il propose plusieurs moyens de définir des fournisseurs personnalisés. Explorons-les.

Valeur des fournisseurs : useValue

La syntaxe useValue est utile pour injecter une valeur constante, placer une bibliothèque externe dans le conteneur Nest ou remplacer une véritable implémentation par un objet simulé. Supposons que vous souhaitiez forcer Nest à utiliser un CatsService factice à des fins de test.

app.module.ts

import { CatsService } from './cats.service';
const mockCatsService = {
/* mise en œuvre simulée
...
*/
};
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
export class AppModule {}

Dans cet exemple, le token CatsService sera résolu à l’objet simulé mockCatsService. useValue nécessite une valeur - dans ce cas, un objet littéral ayant la même interface que la classe CatsService qu’il remplace. En raison de la typologie structurelle de TypeScript, vous pouvez utiliser n’importe quel objet ayant une interface compatible, y compris un objet littéral ou une instance de classe instanciée avec new.

Tokens de fournisseur non basés sur des classes

Jusqu’à présent, nous avons utilisé des noms de classe comme nos tokens de fournisseur (la valeur de la propriété provide dans un fournisseur répertorié dans le tableau providers). Cela correspond au schéma standard utilisé avec l’injection basée sur le constructeur, où le token est également un nom de classe. Parfois, nous pouvons vouloir la flexibilité d’utiliser des chaînes ou des symboles comme token DI.

app.module.ts

import { connection } from './connection';
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
export class AppModule {}

Dans cet exemple, nous associons un token à valeur chaîne ('CONNECTION') à un objet connection préexistant que nous avons importé d’un fichier externe.

Fournisseurs d’alias : useExisting

La syntaxe useExisting vous permet de créer des alias pour des fournisseurs existants. Cela crée deux façons d’accéder au même fournisseur.

app.module.ts

@Injectable()
class LoggerService {
/* détails d'implémentation */
}
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
export class AppModule {}

Fournisseurs de configuration

Un fournisseur peut fournir n’importe quelle valeur, par exemple, un tableau d’objets de configuration basés sur l’environnement actuel.

app.module.ts

const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
export class AppModule {}

Exportation d’un fournisseur personnalisé

Comme tout fournisseur, un fournisseur personnalisé est limité à son module déclarant. Pour qu’il soit visible pour d’autres modules, il doit être exporté.

app.module.ts

const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
export class AppModule {}

Résumé

Les fournisseurs personnalisés dans NestJS vous permettent de gérer des cas d’utilisation avancés, en vous donnant un contrôle granulaire sur l’injection de dépendances. Grâce à des syntaxes comme useValue, useClass, useFactory, et useExisting, vous pouvez adapter le comportement de vos services pour répondre précisément à vos besoins applicatifs.