Services et Injection de Dépendances : Gérer la Logique et les Données
Bienvenue dans cette leçon fondamentale d'Angular, où nous allons explorer deux concepts indissociables et cruciaux pour le développement d'applications robustes et maintenables : les Services et l'Injection de Dépendances (DI). Comprendre et maîtriser ces notions est essentiel pour écrire du code propre, réutilisable et facile à tester dans vos applications Angular.
Introduction : Pourquoi les Services et la DI ?
Lorsque vous avez commencé à développer avec Angular, vous avez probablement mis toute votre logique (récupération de données, manipulation, etc.) directement dans vos composants. C'est une approche simple pour de petites applications, mais elle conduit rapidement à des problèmes de maintenabilité, de réutilisabilité et de testabilité à mesure que votre application grandit.
Imaginez un composant qui gère à la fois l'affichage de l'interface utilisateur (UI) et la récupération de données depuis une API distante. Que se passe-t-il si un autre composant a besoin des mêmes données ? Vous dupliquerez le code. Que se passe-t-il si la façon de récupérer les données change ? Vous devrez modifier tous les composants concernés.
C'est là que les Services entrent en jeu. Ils nous permettent de séparer la logique métier et la gestion des données de la logique de présentation de nos composants.
L'Injection de Dépendances (DI) est le mécanisme puissant qu'Angular utilise pour fournir ces services à nos composants ou à d'autres services, de manière déclarative et efficace. Au lieu qu'un composant crée lui-même ses dépendances, il les déclare, et Angular s'occupe de les lui "injecter".
Ensemble, les services et la DI promeuvent de bonnes pratiques d'ingénierie logicielle telles que la séparation des préoccupations (Separation of Concerns - SoC) et le principe d'inversion de contrôle (Inversion of Control - IoC).
I. Les Services en Angular : Le Cœur de la Logique Métier
Qu'est-ce qu'un Service ?
Un service Angular est une classe TypeScript dont le rôle principal est d'encapsuler une logique métier spécifique, des fonctions utilitaires ou la gestion de l'état des données qui peuvent être partagées à travers plusieurs composants ou d'autres services.
Contrairement aux composants, les services n'ont pas de template et ne sont pas directement liés à l'interface utilisateur. Ce sont des classes pures qui exécutent des tâches.
Les services sont généralement des singletons dans la portée où ils sont fournis, ce qui signifie qu'une seule instance du service est créée et partagée.
Pourquoi Utiliser des Services ?
-
Séparation des préoccupations (SoC) : C'est le bénéfice majeur. Les composants se concentrent uniquement sur l'affichage et l'interaction utilisateur, tandis que les services gèrent les aspects non visuels comme :
- La communication avec des APIs (HTTP).
- La gestion de l'état de l'application.
- Le stockage local des données.
- Des calculs complexes ou des transformations de données.
- La validation de données.
- Des fonctions utilitaires (logging, authentification, etc.).
-
Réutilisabilité : Une fois un service défini, n'importe quel composant ou autre service peut l'injecter et l'utiliser, évitant ainsi la duplication de code.
-
Testabilité : Les services étant de simples classes TypeScript sans dépendances directes sur le DOM ou l'interface utilisateur, ils sont beaucoup plus faciles à tester unitairement de manière isolée. Vous pouvez injecter des versions "mockées" de leurs dépendances pour vous assurer qu'ils fonctionnent correctement.
-
Maintenabilité : En centralisant la logique, les mises à jour ou les corrections de bugs dans un service n'affectent qu'un seul endroit, rendant le code plus facile à maintenir et à faire évoluer.
Création d'un Service
La manière la plus simple de créer un service est d'utiliser l'Angular CLI.
ng generate service users
Cela créera deux fichiers : src/app/users.service.ts et src/app/users.service.spec.ts.
Le contenu de users.service.ts ressemblera à ceci :
// src/app/users.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UsersService {
constructor() { }
getUsers(): string[] {
// Ici, on simule une récupération de données
// En réalité, vous feriez une requête HTTP, par exemple
return ['Alice', 'Bob', 'Charlie'];
}
addUser(name: string): void {
// Logique pour ajouter un utilisateur
console.log(`Ajout de l'utilisateur : ${name}`);
}
}
Explication du Code :
import { Injectable } from '@angular/core';: Ceci importe le décorateur@Injectable.@Injectable(): C'est un décorateur qui marque une classe comme étant éligible pour l'injection de dépendances. Sans ce décorateur, Angular ne pourrait pas "injecter" d'autres services dansUsersServicesi nécessaire.providedIn: 'root': Cette configuration est très importante. Elle indique à Angular que ce service doit être fourni au niveau racine de l'application. Cela signifie qu'Angular va créer une unique instance deUsersServiceet la rendre disponible dans toute l'application. C'est la manière recommandée de fournir des services depuis Angular 6 car elle permet le "tree-shaking" (élimination du code inutilisé) et optimise le bundle final de votre application. Le service est alors un singleton global.export class UsersService { ... }: C'est la définition de notre classe de service.constructor() { }: Le constructeur du service. C'est ici que vous injecteriez d'autres services siUsersServiceen avait besoin.getUsers(): string[] { ... }: Une méthode simple qui simule la récupération d'une liste d'utilisateurs. Dans une application réelle, cette méthode ferait une requête HTTP à un backend.addUser(name: string): void { ... }: Une autre méthode pour démontrer l'encapsulation de logique.
II. L'Injection de Dépendances (DI) en Angular : Comment les Services sont Livrés
Qu'est-ce que l'Injection de Dépendances ?
L'Injection de Dépendances (DI) est un design pattern et un mécanisme utilisé par Angular pour fournir des instances de dépendances (comme nos services) aux objets qui en ont besoin. Au lieu qu'un objet crée ou trouve ses propres dépendances, il déclare les dépendances dont il a besoin, et un "injecteur" (le système DI d'Angular) les lui fournit.
Pensez à un restaurant : vous ne fabriquez pas la chaise sur laquelle vous vous asseyez, ni ne cuisinez votre repas. Vous demandez une chaise, et le restaurant vous la fournit. Vous commandez un plat, et la cuisine vous le prépare et vous l'apporte. Dans cette analogie, le restaurant est le système DI, la chaise et le plat sont les dépendances, et vous (le client) êtes le composant qui les consomme.
Les Fournisseurs (Providers)
Pour qu'Angular puisse injecter un service, il doit savoir comment créer une instance de ce service. C'est le rôle des fournisseurs (providers). Un fournisseur indique au système DI comment obtenir une instance d'une dépendance.
Nous avons déjà vu le moyen le plus courant :
@Injectable({ providedIn: 'root' }):- C'est le mécanisme de provisionnement le plus courant et recommandé.
- Il rend le service disponible dans toute l'application et est optimisé pour le tree-shaking.
- L'instance est un singleton global.
D'autres façons de fournir un service existent, bien que providedIn: 'root' soit préféré pour les services qui doivent être des singletons à l'échelle de l'application :
-
Au niveau d'un Module (
@NgModule) :// src/app/app.module.ts import { NgModule } from '@angular/core'; import { UsersService } from './users.service'; // Importez votre service @NgModule({ // ... providers: [UsersService], // Le service est fourni ici // ... }) export class AppModule { }Si un service est fourni dans un module (autre que le module racine
AppModuleavecprovidedIn: 'root'), il sera un singleton uniquement dans la portée de ce module. Si le module est lazy-loaded, le service sera instancié lors du chargement du module. Si le module est eager-loaded, il se comportera comme un singleton global. -
Au niveau d'un Composant (
@Component) :// src/app/user-list/user-list.component.ts import { Component } from '@angular/core'; import { UsersService } from '../users.service'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'], providers: [UsersService] // Fourni au niveau du composant }) export class UserListComponent { // ... }Si un service est fourni au niveau d'un composant, une nouvelle instance du service est créée chaque fois que le composant est instancié. C'est utile si chaque instance d'un composant a besoin de sa propre instance isolée d'un service. Les composants enfants du composant recevront également cette nouvelle instance.
Consommer un Service (Injection)
Pour utiliser un service dans un composant ou un autre service, vous le déclarez simplement dans le constructor de la classe.
Voici comment un composant utiliserait notre UsersService :
// src/app/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UsersService } from '../users.service'; // Importer le service
@Component({
selector: 'app-user-list',
template: `
<h2>Liste des Utilisateurs</h2>
<ul>
<li *ngFor="let user of users">{{ user }}</li>
</ul>
<button (click)="addUser('David')">Ajouter David</button>
`,
styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
users: string[] = [];
// 1. Injection du service via le constructeur
constructor(private usersService: UsersService) { }
ngOnInit(): void {
// 2. Utilisation du service pour récupérer les données
this.users = this.usersService.getUsers();
}
addUser(name: string): void {
this.usersService.addUser(name);
// Pour voir le changement, il faudrait re-récupérer la liste ou la mettre à jour localement
// Pour cet exemple simple, nous nous contentons d'un log
alert(`L'utilisateur ${name} a été ajouté (voir console).`);
}
}
Explication du Code :
import { UsersService } from '../users.service';: Nous importons la classe du service pour que TypeScript la reconnaisse.constructor(private usersService: UsersService) { }: C'est le cœur de l'injection de dépendances.- Nous déclarons un paramètre
usersServicedans le constructeur. - Nous le typons avec
UsersService. - Angular détecte cette déclaration. Lorsque
UserListComponentest instancié, Angular recherche un fournisseur pourUsersServicedans son système d'injection hiérarchique. - Il trouve l'instance singleton que nous avons fournie avec
providedIn: 'root'et l'injecte dans le constructeur du composant. - L'utilisation du modificateur d'accès
private(oupublic) est un raccourci TypeScript qui déclare et initialise automatiquement une propriété de classe avec le même nom. Cela rendthis.usersServicedisponible dans tout le composant.
- Nous déclarons un paramètre
this.users = this.usersService.getUsers();: Une fois le service injecté, nous pouvons appeler ses méthodes comme n'importe quelle autre méthode d'objet.
III. Concepts Avancés et Bonnes Pratiques
La Portée des Services (Scope)
La portée d'un service détermine où et quand une instance du service est créée et partagée.
-
providedIn: 'root'(Global Singleton) : C'est le plus courant. Le service est un singleton unique dans toute l'application. Idéal pour les services d'authentification, de gestion de données globales, ou des utilitaires généraux. -
Fourni dans un Module (
@NgModule({ providers: [...] })) : L'instance du service est un singleton au sein de ce module. Si ce module est lazy-loaded (chargé à la demande), le service ne sera instancié que lorsque le module est chargé, et il sera un singleton pour toutes les parties de l'application qui consomment ce module. Si le module est eager-loaded (chargé au démarrage), il agit comme un singleton global mais est lié au cycle de vie du module. -
Fourni dans un Composant (
@Component({ providers: [...] })) : Une nouvelle instance du service est créée chaque fois que le composant est créé. Les composants enfants hériteront de cette instance. Utilisez cela lorsque vous avez besoin d'une instance distincte du service pour chaque instance de votre composant (par exemple, un service qui gère l'état d'un formulaire spécifique à un composant).
Services et Gestion d'État
Les services sont excellents pour gérer l'état de l'application, en particulier lorsque cet état doit être partagé entre plusieurs composants. Vous pouvez utiliser des Subjects ou des BehaviorSubjects de RxJS dans vos services pour diffuser des changements d'état aux composants qui s'y abonnent.
// src/app/data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DataService {
private _items = new BehaviorSubject<string[]>(['Item 1', 'Item 2']);
public readonly items$: Observable<string[]> = this._items.asObservable(); // Expose un Observable public
constructor() { }
addItem(newItem: string): void {
const currentItems = this._items.getValue();
this._items.next([...currentItems, newItem]); // Diffuse la nouvelle liste
}
}
Un composant pourrait alors s'abonner aux changements d'items :
// src/app/item-list/item-list.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-item-list',
template: `
<h3>Mes Items</h3>
<ul>
<li *ngFor="let item of (items$ | async)">{{ item }}</li>
</ul>
<button (click)="onAddItem()">Ajouter un Nouvel Item</button>
`
})
export class ItemListComponent implements OnInit {
items$: Observable<string[]>;
constructor(private dataService: DataService) { }
ngOnInit(): void {
this.items$ = this.dataService.items$; // Récupère l'Observable du service
}
onAddItem(): void {
const newItem = `Nouvel Item ${Math.random().toFixed(2)}`;
this.dataService.addItem(newItem);
}
}
Ici, DataService gère la liste d'items. ItemListComponent s'abonne à items$ pour recevoir les mises à jour et affiche la liste en utilisant le pipe async. Quand onAddItem() est appelé, il demande au service d'ajouter un item, et le service notifie tous ses abonnés (y compris ItemListComponent) du changement.
Bonnes Pratiques
- Services à Responsabilité Unique : Chaque service devrait avoir une seule responsabilité principale. Par exemple, un
AuthServicegère l'authentification, unProductsServicegère les produits, etc. - Privilégiez
providedIn: 'root': Utilisez-le par défaut pour vos services à l'échelle de l'application. Cela simplifie la gestion des dépendances et permet le tree-shaking. - Services Stateless vs. Stateful :
- Un service stateless (sans état) effectue une tâche et ne conserve pas d'informations entre les appels. (Ex: un service utilitaire de formatage).
- Un service stateful (avec état) conserve des données et peut les manipuler. (Ex: un
CartServicequi gère les articles du panier). Si votre service est stateful, assurez-vous de bien gérer le cycle de vie de l'état (initialisation, nettoyage).
- Injectez, ne pas instanciez : Ne créez jamais une instance d'un service avec
new YourService(). Laissez le système d'injection de dépendances d'Angular gérer cela. - Types de Fournisseurs Avancés : Angular offre des fournisseurs plus complexes (e.g.,
useClass,useValue,useFactory,useExisting) pour des scénarios avancés, par exemple, pour fournir une implémentation différente d'un service en fonction de l'environnement, ou pour des valeurs de configuration.
Conclusion
Les Services et l'Injection de Dépendances sont au cœur de la philosophie d'Angular. Ils vous permettent de construire des applications web modernes qui sont :
- Modulaires : La logique est bien séparée de l'interface utilisateur.
- Réutilisables : Le code est partagé et non dupliqué.
- Faciles à tester : Les unités de code (services et composants) peuvent être testées indépendamment.
- Maintenables : Les changements et les mises à jour sont plus simples et moins risqués.
- Évolutives : L'architecture est prête à accueillir de nouvelles fonctionnalités sans devenir un "plat de spaghettis".
En maîtrisant ces concepts, vous écrirez du code Angular plus propre, plus efficace et plus professionnel, vous préparant ainsi à construire des applications complexes et robustes avec confiance. Continuez à pratiquer en refactorisant votre logique de composants dans des services dédiés, et vous verrez rapidement les bénéfices.