Injection de dépendances

Notes

Ce livre électronique a été publié au printemps 2017 et n’a pas été mis à jour depuis. Il y a beaucoup dans le livre qui reste précieux, mais une partie du matériel est obsolète.

En règle générale, un constructeur de classe est appelé au moment de l’instanciation d’un objet, et toutes les valeurs dont l’objet a besoin sont passées en tant qu’arguments au constructeur. Il s’agit d’un exemple d’injection de dépendances, plus précisément connu sous le nom d’injection de constructeur. Les dépendances dont l’objet a besoin sont injectées dans le constructeur.

En spécifiant des dépendances en tant que types d’interface, l’injection de dépendances permet le découplage des types concrets du code qui dépend de ces types. Elle utilise généralement un conteneur qui détient une liste d’inscriptions et de mappages entre les interfaces et les types abstraits ainsi que les types concrets qui implémentent ou étendent ces types.

Il existe également d’autres types d’injection de dépendances, tels que l’injection de setter de propriétés et l’injection d’appel de méthode, mais ils sont moins couramment vus. Ainsi, ce chapitre se concentre uniquement sur l’injection de constructeurs avec un conteneur d’injection de dépendances.

Présentation de l’injection de dépendances

L’injection de dépendances est une version spécialisée du modèle IoC (inversion de contrôle), où l’inversion correspond au processus d’obtention de la dépendance nécessaire. Avec l’injection de dépendances, une autre classe est chargée d’injecter les dépendances dans un objet au moment de l’exécution. L’exemple de code suivant montre comment la classe ProfileViewModel est structurée durant l’utilisation de l’injection de dépendances :

public class ProfileViewModel : ViewModelBase  
{  
    private IOrderService _orderService;  

    public ProfileViewModel(IOrderService orderService)  
    {  
        _orderService = orderService;  
    }  
    ...  
}

Le ProfileViewModel constructeur reçoit une IOrderService instance en tant qu’argument, injectée par une autre classe. La seule dépendance de la ProfileViewModel classe concerne le type d’interface. Par conséquent, la ProfileViewModel classe n’a aucune connaissance de la classe responsable de l’instanciation de l’objet IOrderService . La classe chargée d’instancier l’objet et de l’insérer IOrderService dans la ProfileViewModel classe est appelée conteneur d’injection de dépendances.

Les conteneurs d’injection de dépendances réduisent le couplage entre les objets en offrant une fonctionnalité permettant d’instancier les instances de classe, et de gérer leur durée de vie en fonction de la configuration du conteneur. Lors de la création des objets, le conteneur y injecte toutes les dépendances dont l’objet a besoin. Si ces dépendances n’ont pas encore été créées, le conteneur crée et résout d’abord leurs dépendances.

Notes

L’injection de dépendances peut également être implémentée manuellement à l’aide de fabriques. Toutefois, l’utilisation d’un conteneur fournit des fonctionnalités supplémentaires telles que la gestion de la durée de vie et l’inscription via l’analyse d’assembly.

L’utilisation d’un conteneur d’injection de dépendances présente plusieurs avantages :

  • Un conteneur supprime la nécessité pour une classe de localiser ses dépendances et de gérer leurs durées de vie.
  • Un conteneur permet le mappage des dépendances implémentées sans affecter la classe.
  • Un conteneur facilite les tests en permettant de simuler les dépendances.
  • Un conteneur augmente la maintenabilité en facilitant l’ajout de nouvelles classes à l’application.

Dans le contexte d’une Xamarin.Forms application qui utilise MVVM, un conteneur d’injection de dépendances est généralement utilisé pour inscrire et résoudre des modèles d’affichage, ainsi que pour inscrire des services et les injecter dans des modèles d’affichage.

De nombreux conteneurs d’injection de dépendances sont disponibles, l’application mobile eShopOnContainers utilisant TinyIoC pour gérer l’instanciation des classes de modèle d’affichage et de service dans l’application. TinyIoC a été choisi après avoir évalué un certain nombre de conteneurs différents et offre des performances supérieures sur les plateformes mobiles par rapport à la majorité des conteneurs connus. Il facilite la création d’applications faiblement couplées et fournit toutes les fonctionnalités couramment trouvées dans les conteneurs d’injection de dépendances, notamment des méthodes pour inscrire des mappages de types, résoudre des objets, gérer la durée de vie des objets et injecter des objets dépendants dans des constructeurs d’objets qu’il résout. Pour plus d’informations sur TinyIoC, consultez TinyIoC sur github.com.

Dans TinyIoC, le TinyIoCContainer type fournit le conteneur d’injection de dépendances. La figure 3-1 montre les dépendances lors de l’utilisation de ce conteneur, qui instancie un IOrderService objet et l’injecte dans la ProfileViewModel classe.

Exemple de dépendances lors de l’utilisation de l’injection de dépendances

Figure 3-1 : Dépendances lors de l’utilisation de l’injection de dépendances

Au moment de l’exécution, le conteneur doit savoir quelle implémentation de l’interface IOrderService il doit instancier, avant de pouvoir instancier un ProfileViewModel objet. Cela implique les opérations suivantes :

  • Conteneur qui décide comment instancier un objet qui implémente l’interface IOrderService . Cela s’appelle l’inscription.
  • Conteneur instanciant l’objet qui implémente l’interface IOrderService et l’objet ProfileViewModel . Cela s’appelle la résolution.

Finalement, l’application finira d’utiliser l’objet ProfileViewModel et elle deviendra disponible pour le garbage collection. À ce stade, le garbage collector doit se débarrasser de l’instance IOrderService si d’autres classes ne partagent pas les mêmes instance.

Conseil

Écrivez du code indépendant du conteneur. Essayez toujours d’écrire du code indépendant du conteneur pour dissocier l’application du conteneur de dépendance spécifique utilisé.

Inscription

Pour permettre l’injection de dépendances dans un objet, les types des dépendances doivent être inscrits auprès du conteneur. L’inscription d’un type implique généralement de transmettre au conteneur une interface et un type concret qui implémente l’interface.

Il existe deux façons d’inscrire les types et les objets dans le conteneur via du code :

  • Inscrire un type ou un mappage auprès du conteneur. Le cas échéant, le conteneur crée une instance du type spécifié.
  • Inscrire un objet existant dans le conteneur en tant que singleton. Le cas échéant, le conteneur retourne une référence à l’objet existant.

Conseil

Les conteneurs d’injection de dépendances ne conviennent pas toujours. L’injection de dépendances introduit une complexité et des besoins supplémentaires qui ne sont pas toujours appropriés ou utiles pour les petites applications. Si une classe n’a aucune dépendance, ou si elle ne représente pas une dépendance pour d’autres types, il n’est peut-être pas judicieux de la placer dans le conteneur. De plus, si une classe a un seul ensemble de dépendances qui font partie intégrante du type et qui ne changent jamais, il n’est peut-être pas judicieux de la placer dans le conteneur.

L’inscription des types qui nécessitent l’injection de dépendances doit être effectuée dans une seule méthode dans une application, et cette méthode doit être appelée au début du cycle de vie de l’application pour s’assurer que l’application est consciente des dépendances entre ses classes. Dans l’application mobile eShopOnContainers, cela est effectué par la ViewModelLocator classe, qui génère l’objet TinyIoCContainer et est la seule classe de l’application qui contient une référence à cet objet. L’exemple de code suivant montre comment l’application mobile eShopOnContainers déclare l’objet TinyIoCContainer dans la ViewModelLocator classe :

private static TinyIoCContainer _container;

Les types sont inscrits dans le ViewModelLocator constructeur. Pour ce faire, vous devez d’abord créer un TinyIoCContainer instance, ce qui est illustré dans l’exemple de code suivant :

_container = new TinyIoCContainer();

Les types sont ensuite inscrits avec l’objet TinyIoCContainer , et l’exemple de code suivant illustre la forme d’inscription de type la plus courante :

_container.Register<IRequestProvider, RequestProvider>();

La Register méthode présentée ici mappe un type d’interface à un type concret. Par défaut, chaque inscription d’interface est configurée en tant que singleton afin que chaque objet dépendant reçoive les mêmes instance partagés. Par conséquent, une seule RequestProvider instance existera dans le conteneur, qui est partagé par des objets qui nécessitent une injection d’un par le biais d’un IRequestProvider constructeur.

Les types concrets peuvent également être inscrits directement sans mappage à partir d’un type d’interface, comme illustré dans l’exemple de code suivant :

_container.Register<ProfileViewModel>();

Par défaut, chaque inscription de classe concrète est configurée en tant que multi-instance afin que chaque objet dépendant reçoive une nouvelle instance. Par conséquent, lorsque le ProfileViewModel est résolu, une nouvelle instance est créée et le conteneur injecte ses dépendances requises.

Résolution

Une fois qu’un type est inscrit, il peut être résolu ou injecté en tant que dépendance. Lorsqu’un type est en cours de résolution et que le conteneur doit créer un instance, il injecte toutes les dépendances dans le instance.

En règle générale, quand un type est résolu, l’une des trois situations suivantes se produit :

  1. Si le type n’a pas été inscrit, le conteneur lève une exception.
  2. Si le type a été inscrit en tant que singleton, le conteneur retourne l’instance de singleton. Si c’est la première fois que le type est appelé pour, le conteneur le crée si nécessaire et conserve une référence à celui-ci.
  3. Si le type n’a pas été inscrit en tant que singleton, le conteneur retourne une nouvelle instance et ne conserve pas de référence à celui-ci.

L’exemple de code suivant montre comment le RequestProvider type précédemment inscrit auprès de TinyIoC peut être résolu :

var requestProvider = _container.Resolve<IRequestProvider>();

Dans cet exemple, TinyIoC est invité à résoudre le type concret du IRequestProvider type, ainsi que toutes les dépendances. En règle générale, la Resolve méthode est appelée lorsqu’une instance d’un type spécifique est requise. Pour plus d’informations sur le contrôle de la durée de vie des objets résolus, consultez Gestion de la durée de vie des objets résolus.

L’exemple de code suivant montre comment l’application mobile eShopOnContainers instancie les types de modèles d’affichage et leurs dépendances :

var viewModel = _container.Resolve(viewModelType);

Dans cet exemple, TinyIoC est invité à résoudre le type de modèle d’affichage pour un modèle d’affichage demandé, et le conteneur résout également toutes les dépendances. Lors de la résolution du ProfileViewModel type, les dépendances à résoudre sont un ISettingsService objet et un IOrderService objet. Étant donné que les inscriptions d’interface ont été utilisées lors de l’inscription SettingsService des classes et OrderService , TinyIoC retourne des instances singleton pour les SettingsService classes et OrderService , puis les transmet au constructeur de la ProfileViewModel classe. Pour plus d’informations sur la façon dont l’application mobile eShopOnContainers construit des modèles d’affichage et les associe à des affichages, consultez Création automatique d’un modèle d’affichage avec un localisateur de modèles d’affichage.

Notes

L’inscription et la résolution des types avec un conteneur ont un coût en termes de performances en raison de l’utilisation par le conteneur de la réflexion pour créer chaque type, et plus particulièrement si les dépendances sont reconstruites pour chaque navigation entre les pages de l’application. S’il existe de nombreuses dépendances ou des dépendances profondes, le coût de création peut augmenter de manière significative.

Gestion de la durée de vie des objets résolus

Après avoir inscrit un type à l’aide d’une inscription de classe concrète, le comportement par défaut de TinyIoC est de créer une nouvelle instance du type inscrit chaque fois que le type est résolu ou lorsque le mécanisme de dépendance injecte des instances dans d’autres classes. Dans ce scénario, le conteneur ne contient pas de référence à l’objet résolu. Toutefois, lors de l’inscription d’un type à l’aide de l’inscription d’interface, le comportement par défaut de TinyIoC est de gérer la durée de vie de l’objet en tant que singleton. Par conséquent, le instance reste dans l’étendue tant que le conteneur est dans l’étendue et est éliminé lorsque le conteneur sort de l’étendue et est récupéré par le garbage collect, ou lorsque le code supprime explicitement le conteneur.

Le comportement d’inscription TinyIoC par défaut peut être remplacé à l’aide des méthodes Fluent AsSingleton et AsMultiInstance API. Par exemple, la AsSingleton méthode peut être utilisée avec la Register méthode, afin que le conteneur crée ou retourne une instance singleton d’un type lors de l’appel de la Resolve méthode. L’exemple de code suivant montre comment TinyIoC est invité à créer un singleton instance de la LoginViewModel classe :

_container.Register<LoginViewModel>().AsSingleton();

La première fois que le LoginViewModel type est résolu, le conteneur crée un objet LoginViewModel et conserve une référence à celui-ci. Sur toutes les résolutions suivantes de , LoginViewModelle conteneur retourne une référence à l’objet LoginViewModel qui a été créé précédemment.

Notes

Les types inscrits en tant que singletons sont supprimés lorsque le conteneur est supprimé.

Résumé

L’injection de dépendances permet de découpler les types concrets du code qui dépend de ces types. Elle utilise généralement un conteneur qui détient une liste d’inscriptions et de mappages entre les interfaces et les types abstraits ainsi que les types concrets qui implémentent ou étendent ces types.

TinyIoC est un conteneur léger qui offre des performances supérieures sur les plateformes mobiles par rapport à la majorité des conteneurs connus. Il facilite la création d’applications faiblement couplées et fournit toutes les fonctionnalités couramment trouvées dans les conteneurs d’injection de dépendances, notamment des méthodes pour inscrire des mappages de types, résoudre des objets, gérer les durées de vie des objets et injecter des objets dépendants dans des constructeurs d’objets qu’il résout.