
La configuration est l'un des aspects les plus critiques d'une application Angular moderne. Dans ce chapitre, nous allons explorer comment structurer efficacement vos providers, orchestrer l'initialisation de votre application, et intégrer vos dépendances externes de manière robuste.
Mais avant de passer à la pratique, parlons rapidement du principe de l'inversion de dépendances, et pourquoi c'est un concept essentiel pour la gestion des providers.
Le principe d'inversion de dépendances (le "D" de SOLID) dit qu'une classe concrète ne doit pas dépendre d'une autre classe concrète : les deux doivent dépendre d'une abstraction.
Souvent, lorsque le mot abstraction apparaît, ça fait peur ; on entend des cris de "over-engineering !" surgir de l'open-space, alors qu'en vrai c'est très simple et très puissant.
Prenons un cas concret d'une application Angular basique, avec un component smart ProductList et un service Productsqui récupère les produits depuis l'API. Traditionnellement, on fait :
export class ProductList {
private readonly products = inject(Products);
}Ce qui crée une dépendance directe :

Avec l'inversion de dépendances (et grâce à la magie du système d'injection de dépendances d'Angular), on peut changer cette situation pour donner :

Ici, Products devient une abstraction (on verra comment la créer dans quelques instants). ProductList dépend de cette abstraction, et ApiProducts en est l'implémentation concrète.
Comment on fait ?
Voici le setup :
export abstract class Products {
abstract allProducts(): Observable<Product[]>;
}
export class ApiProducts implements Products {
private readonly http = inject(HttpClient);
allProducts(): Observable<Product[]> {
return this.http.get<Product[]>('/api/products');
}
}
export class ProductList {
private readonly products = inject(Products);
products$: Observable<Product[]> = this.products.allProducts();
}Products est une classe abstraite qui déclare les méthodes dont dépendront les components
ApiProducts utilise le mot-clef implements et non extends pour traiter Products comme une interface et non comme une classe parent
ProductsList injecte la classe abstraite et non l'implémentation
Pour tout faire fonctionner, on crée le provider comme ceci :
{ provide: Products, useClass: ApiProducts }Vous voyez ? C'est plutôt facile finalement !
Oui d'accord, mais…pourquoi faire ça ?
L'inversion de dépendances nous offre plusieurs avantages :
découplage entre composants — lorsqu'on pense aux abstractions comme les rôles joués par un objet, l'inversion de dépendances permet de coupler un objet aux rôles de ses dépendances plutôt qu'à leurs implémentations
testabilité améliorée — remplacer les dépendances d'un objet pour l'isoler devient trivial, donc écrire d'excellents tests unitaires devient très simple
flexibilité de configuration — toujours grâce à la facilité de changer l'implémentation des dépendances, on peut, par exemple, avoir une implémentation spécifique pour le développement : imaginez une situation où le backend n'est pas prêt, vous pourrez quand même travailler avec un service qui simule un serveur fonctionnel et simplement remplacer par la vraie implémentation quand l'API est terminé
Garder le DIP en tête vous aidera à prendre de meilleures décisions dans la gestion de vos dépendances.
Les providers contiennent généralement toute la logique métier de vos applications, donc bien les gérer est une priorité. Voici quelques pratiques intéressantes pour cela :
Les injection tokens permettent d'utiliser l'injection de dépendances d'Angular pour fournir autre chose que des objets issus de classes, notamment pour injecter des primitives. Ils sont particulièrement utiles pour injecter des valeurs ou des objets de configuration.
Par exemple, si vous souhaitez établir une valeur par défaut pour la pagination dans votre application, vous pouvez créer un injection token :
export const DEFAULT_PAGE_SIZE = new InjectionToken<number>('Default page size for pagination');Vous pouvez ensuite lui fournir une valeur dans vos providers :
providers: [
{ provide: DEFAULT_PAGE_SIZE, useValue: 10 },
]Et l'exploiter en utilisant inject() :
import { DEFAULT_PAGE_SIZE } from 'core/injection-tokens/default-page-size';
export class StoreProductFacade {
private defaultPageSize = inject(DEFAULT_PAGE_SIZE);
}Cette approche permet notamment de centraliser la configuration, rendant votre application plus facile à maintenir.
Depuis le passage en standalone, Angular fournit ce type de fonction pour plusieurs fonctionnalités : provideHttpClient() , provideZonelessChangeDetection() , provideRouter(routes) … pour faciliter la gestion de vos providers, vous pouvez en faire de même en créant vos propres provider functions !
Imaginez une feature Products (comme ci-dessus), où on aurait un service Products , une façade ProductsFacade , et un store ProductsStore . Vous pouvez créer une fonction comme celle-ci :
export const provideProducts = (): Provider[] => {
return [
{ provide: Products, useClass: ApiProducts },
{ provide: ProductFacade, useClass: StoreProductFacade },
{ provide: ProductStore, useClass: SignalProductStore }
];
}Ensuite, dans le tableau des providers approprié, vous avez juste à appeler :
providers: [
provideProducts()
]Pour les providers au niveau de l'application (dans AppConfig), il faut utiliser une fonction helper fournie par Angular :
export const provideAppCore = (): EnvironmentProviders => {
return makeEnvironmentProviders([
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
]);
}Comme avant, cela permet de faire :
providers: [
provideAppCore()
]Vos providers peuvent également être configurables grâce aux provider factories. Voici un exemple pour la gestion de la configuration de l'application :
type AppConfiguration = {
defaultPageSize: number,
language: 'en' | 'fr'
}
const provideConfiguration = (config: AppConfiguration): Provider[] => {
const providers: Provider[] = [
{ provide: DEFAULT_PAGE_SIZE, useValue: config.defaultPageSize }
];
if (config.language === 'fr') {
providers.push(
{ provide: MatPaginatorIntl, useClass: MatPaginatorIntlFr }
);
}
return providers;
}
export const appConfig: ApplicationConfig = {
providers: [
provideConfiguration({ defaultPageSize: 10, language: 'fr' }),
]
}Ainsi vous pouvez non seulement centraliser la configuration des valeurs de votre application, vous pouvez également configurer les dépendances qui sont utilisées. Dans l'extrait ci-dessus :
on configure une injection token avec une valeur primitive
si la langue sélectionnée est l'anglais, il n'y a pas de dépendance à ajouter
si la langue sélectionnée est le français, il faut ajouter la dépendance d'internationalisation
Si vous avez des méthodes que vous devez absolument appeler au démarrage de l'application, comme l'initialisation d'un service par exemple, Angular fournit la fonction provideAppInitializer (qui remplace l'ancien APP_INITIALIZER ). Vous pouvez donc, par exemple, faire ceci :
@Injectable()
export class Config {
init(): void {
// do init here
}
}
export const appConfig: ApplicationConfig = {
providers: [
provideAppInitializer(() => {
const config = inject(Config);
config.init();
}),
Config
]
}Ainsi avant un quelconque chargement de component, la méthode init() sera appelée.
L'inversion de dépendances montre particulièrement son utilité lorsqu'on souhaite remplacer une implémentation par un test double pour mieux isoler le système que l'on teste. Par exemple, si vous avez un component ProductsList qui appelle un ProductsFacade pour récupérer la liste de produits, vous pouvez simplifier les tests de votre component en lui fournissant un stub :
export class FakeProductsFacade implements ProductsFacade {
productList(): Signal<Product[]> {
return signal([
// fake list of products
]);
}
}
TestBed.configureTestingModule({
imports: [
ProductsList
],
providers: [
{ provide: ProductsFacade, useClass: FakeProductsFacade }
]
})Sans oublier que dans ProductsList, c'est l'abstraction qui y est injectée et non l'implémentation :
export class ProductsList {
private productsFacade = inject(ProductsFacade);
}Du coup, à l'intérieur de notre fichier de test, c'est le stub qui sera injectée et non l'implémentation de production.
Un chapitre sur les dépendances ne serait jamais complet sans parler des dépendances tierces : ces libraries que nous utilisons pour accélérer le développement et éviter de réinventer la roue MAIS qui sont souvent source de problèmes : mises à jour compliquées, breaking changes, évolution de nos besoins…
On peut éviter un certain nombre de ces problèmes en créant des adapters (ou wrappers) autour de ces dépendances. Ça nous permet de modifier l'implémentation à tout moment (et même de changer de library) sans casser quoique ce soit dans nos applications.
Prenons un exemple : les Dialog d'Angular Material. Plutôt qu'injecter Dialog directement dans vos components, vous utilisez l'inversion de dépendances et créez une classe abstraite et son implémentation Material :
export interface DialogBox {
get closed(): Observable<void>;
}
export abstract class Dialogs {
abstract open(component: any): DialogBox {}
}
@Injectable()
export class MaterialDialogs implements Dialogs {
private matDialog = inject(Dialog); // Material dependency
open(component: any): Observable<DialogBox> {
return this.dialog.open(component);
}
}Et pour le provider, vous commencez à en avoir l'habitude :
providers: [
{ provide: Dialogs, useClass: MaterialDialogs }
]Comme ça, le jour où Material modifie son implémentation, vous n'avez qu'à modifier celle de MaterialDialogs et le reste de votre application continuera de fonctionner, tout comme si un jour vous souhaitez changer de library.

Vous héritez d'un projet Angular où un componentUserProfileinjecte directement les servicesHttpClient,AuthService, etNotificationService. Le component fait des appels HTTP, vérifie les permissions, et affiche des messages à l'utilisateur. L'équipe se plaint que les tests sont difficiles à écrire et que le code est rigide.
Identifiez les problèmes d'architecture dans cette approche et proposez une refactorisation en appliquant le principe d'inversion de dépendances. Décrivez les abstractions que vous créeriez, les providers nécessaires, et expliquez comment cette approche améliorerait la testabilité et la flexibilité du code.
La principe d'inversion de dépendances : dépendez d'abstractions plutôt que d'implémentations concrètes pour découpler vos composants et améliorer la testabilité
Utilisez des provider functions et des injection tokens pour centraliser et structurer la configuration de vos dépendances
ExploitezprovideAppInitializer()pour orchestrer le démarrage de votre application et gérer les dépendances critiques
Créez des adapters autour des librairies externes pour éviter le couplage fort et faciliter les évolutions futures
Maintenant que votre application est solidement configurée avec des dépendances bien structurées, il est temps de passer à l'étape suivante : mettre en place un workflow de développement efficace et déployer votre application en production de manière robuste et automatisée.