Intermédiaire

Injection de dépendances : retour d’expérience

Nninject A l’école ou en formation, on apprend à développer en utilisant des concepts avancés comme l’inversion de contrôle, qui ne sont pas toujours mis en place dans les projets de l’équipe que vous rejoignez en mission.

Un projet tout neuf est l’occasion rêvée d’introduire de nouvelles choses et de former l’équipe de développement qui a parfois d’autres habitudes.

Nous allons voir ensemble un retour d’expérience sur la mise en place du principe de l’inversion de contrôle sur un projet ASP.NET MVC avec le framework d’injection de dépendances Ninject.

L’injection de dépendances

Les classes d’une application ont souvent un certain nombre de dépendances. Ce peut être une classe d’une librairie externe, comme une librairie de log par exemple, ou, dans le cas d’une application multi-tiers, on peut penser aux dépendances entre les couches de l’application.

Des dépendances fortes peuvent poser problème, notamment quand on veut utiliser telle ou telle librairie selon le cas, ou bien tout simplement lorsqu’on veut tester son application multi-tiers. Le principe du pattern d’inversion de contrôle est de faire en sorte que les classes ne soient pas responsables de l’instanciation des services dont elles sont dépendantes.

L’injection de dépendances est le mécanisme qui permet de mettre en place une inversion de contrôle, en injectant dynamiquement les dépendances des classes en utilisant un ou plusieurs modules d’interface commune. On fournit à nos objets des services instanciés, prêts à être utilisés.

Mise en place du projet

La première tâche est de trouver quel framework utiliser parmi tous ceux disponibles sur le marché, chacun ayant des avantages et inconvénients. Sa facilité de mise en pratique a fait pencher la balance vers Ninject : il dispose d’une API très intuitive et le mettre en place sur la solution se résume à 2 actions : l’ajout du package Nuget et la configuration des modules de binding. Nécessitant peu de documentation à lire pour des novices en inversion de contrôle, ceux-ci n’auront qu’une ligne à ajouter dans les modules concernés au fur et à mesure des développements. Il est aussi très discret : on n’en fait aucune mention en dehors du projet web et de celui qui abrite les modules.

projets

La solution se constitue concrètement d’un projet web MVC, ainsi que de deux projets de classes C#, Business et DataAccess. Cette dernière s’appuyant sur Entity Framework pour l’accès à la base de données.
A cela s’ajoute un projet DependencyResolver ayant pour rôle de résoudre les dépendances entre les couches même les plus profondes sans que le projet web n’ait connaissance de celles-ci. C’est ici que se situent nos modules personnalisés de Ninject, que les développeurs enrichiront au fur et à mesure des développements. L’objectif étant de mettre en place des tests unitaires, une configuration via un fichier XML n’est pas nécessaire et encombrerait la configuration de l’application inutilement.

Il a été dans un premier temps demandé à l’équipe de concentrer ses efforts sur les tests unitaires des couches Business et DataAccess. Effort permet de simuler très facilement Entity Framework afin de tester les requêtes Linq de la couche d’accès aux données. Quant aux tests sur la couche Business, ils seront isolés de la couche d’accès grâce à l’utilisation du framework Moq.

Accueil de l’équipe

fear-1180118_640L’équipe de développement a été plutôt intéressée et enthousiaste par cette nouveauté, malgré quelques grimaces au début, notamment dues aux réserves sur le changement d’habitude qui a parfois été interprété comme de petites pertes de temps. Par exemple, quand on fait F12 sur le nom d’une méthode, on ne tombe plus sur l’implémentation mais sur l’interface ; mais aussi des interrogations sur l’impact d’un tel système sur les performances de la future application.

Chaque développeur a toutefois fait l’effort de s’intéresser aux nouvelles consignes : une interface par classe, ne plus instancier les services manuellement, penser à développer les tests unitaires en parallèle, etc. Des solutions permettent de pallier les petits désagréments, et tout le monde a finalement fait preuve de bonne volonté et a été attentif.

Impacts organisationnels

L’injection de dépendances n’étant pas du tout implantée dans les habitudes de l’équipe, les tests unitaires étaient jusque-là très limités, pour ne pas dire impossibles au niveau de certaines couches, sur de précédents projets. Aucun des développeurs n’avait eu l’occasion de réellement travailler dans une démarche orientée tests, c’était une nouveauté pour tous. Tout le monde avait plutôt l’habitude de développer toutes les couches du projet en parallèle, et de tester le fonctionnement grâce au navigateur web.

L’introduction de l’injection de dépendance a permis d’envisager l’abandon de cette pratique en faveur d’un développement couche par couche et orienté test. L’adoption de ce nouveau mode de travail est fastidieuse car on a l’impression d’aller beaucoup plus vite en oubliant les tests dans un premier temps, et en sortant une release en un temps record. Les tests unitaires seraient développés « après », « quand on aura plus de temps ». Cela a des impacts considérables sur la qualité des tests : il faut « se remettre dans le bain » à postériori, on oublie des cas de test, et ce n’est pas du tout un travail agréable à faire. Les tests peuvent alors être bâclés et leur efficacité amoindrie.

Correction de problèmes sous-jacents et impacts sur la qualité des développements

L’introduction de tests plus poussés a notamment révélé au sein de l’équipe une difficulté à séparer les responsabilités des différentes couches du projet et à utiliser les mocks en conséquence dans les tests unitaires.

Par exemple, dans un service Business, on désire loguer chacune des modifications faites sur une entité Employee. Nous avons donc une classe EmployeeService avec injection d’un repository ainsi qu’un service de log.

public class EmployeeService : IEmployeeService
{
        private IEmployeeRepository _repository;

        private ILogger _logger;

        public EmployeeService(IEmployeeRepository repository, ILogger logger)
        {
                _repository = repository;
                _logger = logger;
        }

        public bool Edit(Employee employee, string loginUser)
        {
                this._logger.AddHistory("Edit employee", employee.Id, loginUser);            
                return this._repository.Edit(employee);
        }
}

Dans la méthode de test correspondante, on mockera donc les interfaces IEmployeeRepository et ILogger pour tester strictement le comportement de la méthode EmployeeService.Edit.

L’idée est donc de vérifier dans nos tests unitaires :

  • Si la méthode IEmployeeRepository.Edit a été appelée
  • Si la méthode ILogger.AddHistory a été appelée
  • La valeur de retour de la méthode en fonction de celle de IEmployeeRepository.Edit

« Mais alors, comment je teste que l’employé a bien été mis à jour et que le log a bien été ajouté ? »
Ce n’est pas la responsabilité de la méthode EmployeeService.Edit, on se contente de vérifier que les méthodes des deux interfaces ont été appelées avec les paramètres nécessaires.

Un autre effet sur la qualité des développements est une approche plus perfectionniste, plus propre, du code. Le refactoring étant très facilité par l’existence des tests unitaires, les développeurs reviennent plus volontiers sur leurs classes et redécoupent les méthodes, les fusionnent, renomment telle ou telle variable, afin d’augmenter la maintenabilité de l’application.

Etat des lieux à la fin du projet

Changer de méthode de travail nécessite un certain investissement pour répondre aux sollicitations de l’équipe et valider l’utilisation de telle ou telle technique au jour le jour. Mais le jeu en vaut la chandelle : couverture de tests complète, peu d’effets de bord au cours du développement des différents lots successifs (pour la plupart, ils étaient détectés lorsque les tests unitaires étaient lancés, régulièrement et à chaque commit) et par extension, une démarche de code plus propre. On peut aisément affirmer que les éventuelles évolutions, qui seront demandées dans les mois qui viennent, seront faciles à ajouter et ne déstabiliseront pas l’application existante.

Bien qu’un temps d’adaptation ait été nécessaire, ce n’est pas tant l’injection de dépendances en elle-même qui a été difficile à appréhender pour l’équipe, mais la petite révolution que cela a engendré au niveau organisationnel, en grande partie liée aux tests unitaires poussés que cela a permis. Un petit rappel a été nécessaire de temps en temps, car les mauvaises habitudes reprennent vite le dessus : un mail d’informations sur les points assimilés et le travail qui reste à faire, après chaque revue de code, a été une méthode plutôt efficace et bien perçue.

Au moment de la mise en production de l’application, le processus n’est évidemment pas tout à fait au point, il s’agit d’un premier jet. Mais les développeurs ont dans l’ensemble compris l’intérêt de la démarche et s’y mettent volontiers. Un autre projet est d’ailleurs en cours de modification pour y intégrer l’injection de dépendance, classe par classe, au fur et à mesure des évolutions et de l’écriture des tests unitaires. Le principe de l’injection de dépendances dans une application étant maintenant bien assimilé par l’équipe, nous comptons tester des frameworks plus performants que Ninject, tel qu’Autofac ou Unity.

© SOAT
Toute reproduction interdite sans autorisation de la société SOAT

Nombre de vue : 1681

AJOUTER UN COMMENTAIRE