Injection de Dépendances (Dependency Injection - DI)
Introduction
L’injection de dépendances (DI) est un principe fondamental de l’architecture logicielle visant à découpler les composants d’une application. Elle permet de centraliser la gestion des dépendances en évitant la création d’instances directement dans les classes, améliorant ainsi la testabilité, la maintenabilité et la flexibilité du code.
Prérequis : Inversion de Contrôle (IoC)
Avant de comprendre pleinement l’injection de dépendances, il est essentiel de maîtriser le concept d’Inversion de Contrôle (IoC). IoC est un principe selon lequel le contrôle de la création des objets et de leur gestion de cycle de vie est transféré d’un programme à un conteneur externe. DI est l’une des implémentations de l’IoC, où les dépendances sont injectées dans une classe plutôt que créée en son sein. Comprendre IoC permet de mieux appréhender les mécanismes qui sous-tendent DI et d’en saisir toute l’efficacité dans la gestion des dépendances.
Principe
Considérez l’application Uber. Au lieu de créer directement une instance du service de cartographie Google Maps au sein de son code, l’application reçoit ce service en tant que dépendance (une application android). Imaginez qu’elle ait une interface ServiceDeCartographie avec des méthodes comme calculerItinéraire() ou afficherCarte(). L’application ne se soucie pas de l’implémentation concrète (Google Maps, OpenStreetMap, etc.) ; elle utilise simplement l’interface.
Au démarrage de l’application, le conteneur IoC fournit l’implémentation de ServiceDeCartographie à l’application. Cet injecteur peut être configuré pour utiliser Google Maps en production et un service de test en développement. Si Uber décide de changer de fournisseur de cartes, il suffit de modifier la configuration de l’injecteur, sans toucher au code de l’application.
L’application est ainsi découplée du service de cartographie spécifique, ce qui la rend plus flexible et testable. Elle ne “crée” pas ses dépendances, elle les “reçoit” par injection, d’où le terme “injection de dépendances”.
Pourquoi utiliser l’Injection de Dépendances ?
- Découplage : Réduis la dépendance forte entre les classes.
- Testabilité : Facilite les tests unitaires en remplaçant facilement les dépendances par des mocks.
- Flexibilité : Permets de changer l’implémentation d’une dépendance sans modifier le code consommateur.
- Gestion centralisée des dépendances : Évite la duplication de code.
Réduction du Couplage avec DI
Le couplage fort entre les classes rend une application rigide et difficile à maintenir. Sans DI, une classe instancie directement ses dépendances, ce qui crée un lien fort et rend difficile le remplacement ou la modification de ces dépendances.
Exemple de Couplage fort (sans DI)
public class Client
{
private MonService _monService;
public Client()
{
_monService = new MonService();
}
}
Dans cet exemple, Client
dépend directement de MonService
. Toute modification de MonService
peut impacter Client
, et il est difficile de tester Client
avec une version mockée de MonService
.
Réduction du Couplage avec DI
Avec la DI, Client
ne crée plus directement son service, mais le reçoit en paramètre, ce qui permet d’injecter une implémentation différente si nécessaire.
public class Client
{
private readonly IMonService _monService;
public Client(IMonService monService)
{
_monService = monService;
}
}
Maintenant, Client
ne dépend plus de MonService
mais de son abstraction IMonService
, ce qui permet :
- D’utiliser différentes implémentations sans modifier
Client
. - De faciliter les tests en injectant un mock.
- D’améliorer la modularité et la maintenabilité du code.
Exemple
classDiagram
class IContainerRegistry {
+Register(IT, t)
+Resolve(IT)
}
class IMonService {
+FaireQuelqueChose()
}
class MonService {
+FaireQuelqueChose()
}
class Client {
- monService: IMonService
+ Client(IMonService)
}
class IClient {
- monService: IMonService
+ Client(IMonService)
}
class ClassExterne {
+ Map()
}
IContainerRegistry ..> Client : Injection
IMonService <|-- MonService : Implémente
Client --> IMonService : Injection
IClient <|-- Client : Implémente
IContainerRegistry--> ClassExterne : Register & Resolve
Explication :
IContainerRegistry enregistre (Register<T>) et résout (Resolve<T>) les services.
IMonService est une abstraction, implémentée par MonService.
MonService implémente IMonService et contient la logique métier.
Client dépend de IMonService et reçoit son instance injectée.
Une classe externe s’occupe de mapper les interface à leur implémentation via IContainerRegistry.Register<Interface, Implémentation>()
containerRegistry.RegisterSingleton<IMonService, MonService>();
containerRegistry.RegisterSingleton<IClient, Client>();
Puis lorsqu’on appelle :
var client = containerRegistry.Resolve<IClient>();
Le container va résoudre IClient en new client()
qu’il va instancier et automatiquement injecter un new MonService()
dans le constructeur. IContainerRegistry est responsable de la gestion des cycles de vie des objets (par exemple, s’ils sont des singletons ou des instances transitoires).
Le Client ne connaît pas MonService directement, mais utilise IMonService, ce qui favorise l’abstraction et la testabilité.
On pourrait avoir deux classes implémentant IMonService (ex. MonServiceA et MonServiceB) et choisir au lancement celle à utiliser via IContainerRegistry, offrant ainsi une flexibilité accrue et facilitant l’évolution du code.
Concepts Clés
1. Service et Client
Un service est une classe qui fournit une fonctionnalité spécifique, tandis qu’un client est une classe qui consomme ce service.
public interface IMonService
{
void FaireQuelqueChose();
}
public class MonService : IMonService
{
public void FaireQuelqueChose()
{
Console.WriteLine("Service exécuté.");
}
}
2. Injection par Constructeur
La manière la plus courante d’injecter une dépendance est via le constructeur.
public class Client
{
private readonly IMonService _monService;
public Client(IMonService monService)
{
_monService = monService;
}
}
3. Injection par Propriété
Une autre approche consiste à injecter la dépendance via une propriété publique.
public class Client
{
public IMonService MonService { get; set; }
}
4. Injection par Méthode
Les dépendances peuvent aussi être passées en paramètre d’une méthode.
public class Client
{
public void SetService(IMonService monService)
{
monService.FaireQuelqueChose();
}
}
Utilisation avec un Conteneur DI
Un framework DI comme Prism permet d’automatiser l’injection des dépendances.
Étape 1 : Configuration du conteneur
using Prism.Ioc;
using Prism.Unity; // Ou Prism.DryIoc
using Xamarin.Forms;
public class App : PrismApplication
{
public App() : base() { }
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterSingleton<IMonService, MonService>();
}
protected override void OnInitialized()
{
NavigationService.NavigateAsync("MainPage");
}
}
Étape 2 : Injection dans une Vue-Model
public class MainPageViewModel
{
private readonly IMonService _monService;
public MainPageViewModel(IMonService monService)
{
_monService = monService;
}
}
Résumé
- L’injection de dépendances réduit le couplage entre les composants.
- Elle peut se faire via le constructeur, la propriété ou la méthode.
- L’utilisation d’un conteneur DI permet d’automatiser et de centraliser la gestion des dépendances.
- Prism, parmi d’autres frameworks, facilite l’implémentation de DI dans une application C#.