Tout d’abord, il y a une grande différence entre Dependency Injection et Inversion of Control. DI c’est un patron de conception, tandis que IoC c’est plutôt un mécanisme, ou une librairie(Package Nuget) utilisé pour injecter automatiquement des dépendances. NINJECT ou Unity sont des exemples de librairies IoC.
La modularité et la flexibilité de ASP.NET Core s’appuient énormément sur l’injection de dépendances qui est désormais une caractéristique essentielle du Framework. En effet, ASP.NET implémente de façon native la gestion de l’injection de dépendances. Le développeur n’aura plus besoin d’outils tiers pour mettre en place cette bonne pratique orientée objet.
L’injection de dépendances peut à première vue sembler abstraite et pourrait même faire peur au développeur. Mais, son support natif par ASP.NET Core oblige tout développeur qui utilise le Framework à maitriser les principes de cette pratique et comment elle est mise en place dans ASP.NET Core.
Ne pas comprendre l’intérêt et l’implémentation de l’injection de dépendances peut ouvrir la voie à de nombreux travers et de mauvaises pratiques. Par exemple, le fait de disposer d’un conteneur d’inversion de contrôle (IoC) et l’utiliser ne signifie pas pour autant pratiquer de l’injection de dépendances. C’est quelque chose d’assez fréquent chez les personnes qui ne maitrisent pas de façon concrète cette bonne pratique.
Pourquoi l’injection des dépendances ?
Pour faire ressortir l’intérêt de l’injection de dépendances, je vais faire une analogie avec le code spaghetti très souvent utilisé en programmation. Le code mal écrit c’est comme un plat de spaghetti. « Il suffit de tirer sur un fil d’un côté de l’assiette pour que l’enchevêtrement des fils provoque des mouvements jusqu’au côté opposé. » Un code fortement couplé (avec des composants qui dépendent de nombreux autres composants) peut être sujet à de nombreux bogues, est difficilement maintenable et à comprendre. Une modification à un composant peut facilement impacter de nombreux autres composants.
Pourtant, en programmation orientée objet, des objets travaillent ensemble dans un modèle de collaboration ou il y a des contributeurs et des consommateurs. Il va de soi que ce modèle de programmation génère des dépendances entre les objets et les composants, devenant difficile à gérer lorsque la complexité augmente.
De plus, mettre en place des tests unitaires pour un code fortement couplé peut s’avérer très difficile.
Pour pallier cela, vous pouvez utiliser l’injection de dépendances. L’injection de dépendances prône le découplage entre les composants. En effet, un composant B doit dépendre d’une abstraction d’un composant A. Ainsi, le composant B n’a pas besoin de se préoccupé de comment le composant A est implémenté. De ce fait, l’implantation de A ou les changements qui pourront être apportés à ce dernier vont moins impacter le composant B.
De façon concrète, supposons que nous disposons d’une classe d’accès aux données (CategoriesRepository) pour la manipulation des catégories.
Sans injection des dépendances, l’exemple standard d’utilisation de cette classe devrait ressembler à ceci :
En appliquant l’injection de dépendances, le code ci-dessus devrait ressembler à ce qui suit :
Il existe plusieurs types d’injection de dépendances mais on ne va évoquer que les plus fréquentes :
Injection de dépendances par constructeur : (fonctionne de la même façon dans un controller API ou MVC )
L’injection de dépendances par constructeur est l’approche la plus couramment utilisée dans la mise en place de la pratique. Il s’agit simplement de passer la dépendance comme paramètre du constructeur. Son implémentation simple, que nous avons déjà présentée plus haut, est la suivante :
L’injection par constructeur permet d’éviter l’utilisation d’une dépendance qui n’a pas été initialisée comme c’est le cas avec l’injection par propriété. À l’initialisation de cette classe, le développeur sera obligé de fournir une instance de la dépendance.
Conteneurs d’injection de dépendances :
Cette méthode consiste à tout injecter via une classe spécifique souvent dans le Startup.cs.
La configuration du conteneur d’IoC consiste en l’ajout de vos services (interface et implémentation) dans le catalogue des types abstraits. Chaque fois que vous avez un type abstrait qui doit être résolu par le conteneur IoC, vous devez enregistrer ce dernier.
L’enregistrement des services se fait via la méthode ConfigureServices (IServiceCollection) de la classe Startup de votre projet.
L’ajout d’un nouveau service à la collection des services du conteneur IoC se fait en utilisant la méthode IServiceCollection comme suit :
services.Add(new ServiceDescriptor(typeof(ICategoriesRepository), typeof(CategoriesRepository), ServiceLifetime.Transient));
Le premier paramètre est le type abstrait (l’interface) du service, le deuxième est l’implémentation de ce dernier et le troisième comment il sera instancié.
Le troisième paramètre correspond au cycle de vie du service.
Cycle de vie des Services
Le conteneur d’IoC a désormais la responsabilité de fournir les instances des services qui sont utilisés par l’application. Doit-il fournir la même instance d’un service à tous les objets qui l’utilisent ? Doit-il fournir à chaque objet une instance différente du service ?, etc.
ASP.NET Core offre trois options pour définir comment les services sont instanciés pour les objets appelant. Il s’agit de :
- Transient;
- Scoped;
- Singleton.
Transient
Lorsqu’un service est enregistré dans le conteneur d’IoC avec Transient, cela signifie que pour chaque objet qui fera appel à ce service, le conteneur d’IoC va fournir une instance de ce dernier. Ce qui signifie qu’une instance du service ne sera jamais partagée à plus d’un objet par le conteneur d’IoC.
ASP.NET offre l’extension AddTransient pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :
services.AddTransient<ICategoriesRepository, CategoriesRepository>();
Scoped
L’enregistrement d’un service avec Scoped signifie que celui-ci sera instancié à chaque requête. Concrètement, pour une requête HTTP vers l’application, tous les objets qui utilisent le service recevront la même instance de ce dernier du conteneur d’IoC.
ASP.NET offre l’extension AddScoped pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :
services.AddScoped<ICategoriesRepository, CategoriesRepository>();
Singleton
Singleton est utilisé pour un service qui doit être instancié une seule fois et dont la même instance sera utilisée par tous les composants de l’application qui en auront besoin. Le service est créé pour le premier composant qui en fait la demande, et utilisé pour le reste.
Si votre application nécessite un Singleton, au lieu d’implémenter le pattern Singleton, il est recommandé d’utiliser ce cycle de vie offert par le conteneur d’IoC.
ASP.NET offre l’extension AddSingleton pour l’utilisation de ce dernier. Pour enregistrer un service, vous devez procéder comme suit :
services.AddSingleton<ICategoriesRepository, CategoriesRepository>();
Autres extensions offertes par le conteneur d’IoC
ASP.NET Core offre de nombreuses autres extensions pour l’enregistrement des autres fonctionnalités de la plateforme. Il s’agit notamment de AddMVC pour l’enregistrement du service pour le support de ASP.NET Core MVC, AddDbContext pour Entity Framework et AddIdenty pour l’authentification.
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
// Add framework services.
services.AddEntityFrameworkSqlite()
.AddDbContext<BlogContext>(options => options.UseSqlite("Data Source=Blog.db"));
services.AddMvc();
// Add application services. services.Add<ICategoriesRepository, CategoriesRepository, Transient>();
}
Injection de dépendances dans une action
Il peut arriver que le service dont vous avez besoin ne soit utilisé que par une seule action du contrôleur. Au lieu de passer celui-ci en paramètre au constructeur, vous pouvez directement passer ce dernier en paramètre à la méthode d’action qui l’utilise en ayant recours à l’attribut [FromServices] comme suit :
public IActionResult Index([FromServices] ICategoriesRepository categoriesRepository)
{
return View(categoriesRepository.GetAll());
}
Maintenant que vous en savez un peu plus sur l’injection de dépendances, il ne vous plus qu’à la mettre en application dans vos futurs développements.
Si vous utilisez du .NET Core, cela deviendra un réflexe naturel 🙂
Articles similaires