Injection de dépendance pour les débutants

Bonjour, Habr!



Nous nous préparons à sortir la deuxième édition du livre légendaire de Mark Siman " Dependency Injection on the .NET Platform ".







Même dans un livre aussi volumineux, il n'est guère possible de couvrir complètement un tel sujet. Mais nous vous proposons une traduction abrégée d'un article très accessible qui décrit l'essence de l'injection de dépendances dans un langage simple - avec des exemples en C #.



Le but de cet article est d'expliquer le concept d'injection de dépendances et de montrer comment il est programmé dans un projet donné. De Wikipedia:



L'injection de dépendances est un modèle de conception qui sépare le comportement de la résolution des dépendances. Ainsi, il est possible de détacher des composants fortement dépendants les uns des autres.


L'injection de dépendances (ou DI) vous permet de fournir des implémentations et des services à d'autres classes pour la consommation; le code reste très faiblement couplé. Le point principal dans ce cas est le suivant: à la place des implémentations, vous pouvez facilement substituer d'autres implémentations, et en même temps vous devrez changer un minimum de code, puisque l'implémentation et le consommateur sont liés, très probablement, uniquement par un contrat .



En C #, cela signifie que vos implémentations de service doivent répondre aux exigences de l'interface, et lors de la création de consommateurs pour vos services, vous devez cibler l' interface , pas l'implémentation, et exiger que l' implémentation vous soit fournie ou implémentée.afin que vous n'ayez pas à créer les instances vous-même. Avec cette approche, vous n'avez pas à vous soucier au niveau de la classe de la manière dont les dépendances sont créées et d'où elles proviennent; dans ce cas, seul le contrat est important.



Injection de dépendances par exemple



Regardons un exemple où DI peut être utile. Tout d'abord, créons une interface (contrat) qui nous permettra d'effectuer une tâche, par exemple, enregistrer un message:



public interface ILogger {  
  void LogMessage(string message); 
}
      
      





Remarque: cette interface ne décrit nulle part comment un message est consigné et où il est consigné; ici, l'intention est simplement passée d'écrire la chaîne dans un référentiel. Ensuite, créons une entité qui utilise cette interface. Disons que nous créons une classe qui garde la trace d'un répertoire spécifique sur le disque et, dès qu'une modification est apportée au répertoire, elle enregistre le message correspondant:



public class DirectoryWatcher {  
 private ILogger _logger;
 private FileSystemWatcher _watcher;

 public DirectoryWatcher(ILogger logger) {
  _logger = logger;
  _watcher = new FileSystemWatcher(@ "C:Temp");
  _watcher.Changed += new FileSystemEventHandler(Directory_Changed);
 }

 void Directory_Changed(object sender, FileSystemEventArgs e) {
  _logger.LogMessage(e.FullPath + " was changed");
 }
}
      
      





Dans ce cas, il est très important de noter que nous sommes fournis avec le constructeur dont nous avons besoin, qui implémente ILogger



. Mais, encore une fois, notez: nous ne nous soucions pas de l'emplacement du journal ni de la manière dont il est créé. Nous pouvons simplement programmer avec l'interface à l'esprit et ne penser à rien d'autre.



Ainsi, pour créer une de nos instances DirectoryWatcher



, nous avons également besoin d'une implémentation prête à l'emploi ILogger



. Allons-y et créons une instance qui enregistre les messages dans un fichier texte:



public class TextFileLogger: ILogger {  
 public void LogMessage(string message) {
  using(FileStream stream = new FileStream("log.txt", FileMode.Append)) {
   StreamWriter writer = new StreamWriter(stream);
   writer.WriteLine(message);
   writer.Flush();
  }
 }
}
      
      





Créons-en un autre qui écrit des messages dans le journal des événements Windows:



public class EventFileLogger: ILogger {  
 private string _sourceName;

 public EventFileLogger(string sourceName) {
  _sourceName = sourceName;
 }

 public void LogMessage(string message) {
  if (!EventLog.SourceExists(_sourceName)) {
   EventLog.CreateEventSource(_sourceName, "Application");
  }
  EventLog.WriteEntry(_sourceName, message);
 }
}
      
      





Nous avons maintenant deux implémentations distinctes qui consignent les messages de manière très différente, mais les deux le font ILogger



, ce qui signifie que l'une ou l'autre peut être utilisée partout où une instance est nécessaire ILogger



. Ensuite, vous pouvez créer une instance DirectoryWatcher



et lui dire d'utiliser l'un de nos enregistreurs:



ILogger logger = new TextFileLogger();  
DirectoryWatcher watcher = new DirectoryWatcher(logger);
      
      





Ou, simplement en changeant le côté droit de la première ligne, nous pouvons utiliser une implémentation différente:



ILogger logger = new EventFileLogger();  
DirectoryWatcher watcher = new DirectoryWatcher(logger);
      
      





Tout cela se produit sans aucune modification de l'implémentation de DirectoryWatcher, et c'est la chose la plus importante. Nous injectons notre implémentation de logger dans le consommateur afin que celui-ci n'ait pas à créer lui-même une instance. L'exemple montré est trivial, mais imaginez ce que ce serait d'utiliser de telles techniques dans un projet à grande échelle où vous avez plusieurs dépendances et où beaucoup plus de consommateurs les utilisent. Et puis, soudainement, il y a une demande de modification de la méthode qui enregistre les messages (par exemple, les messages doivent maintenant être enregistrés sur le serveur SQL à des fins d'audit). Si vous n'utilisez pas l'injection de dépendances sous une forme ou une autre, vous devrez examiner attentivement le code et apporter des modifications là où le journal est réellement créé puis utilisé. Sur un grand projet, un tel travail peut être fastidieux et sujet aux erreurs.Avec DI, vous changez simplement la dépendance en un seul endroit, et le reste de l'application absorbera les modifications et commencera immédiatement à utiliser la nouvelle méthode de journalisation.



En substance, il résout le problème classique de dépendance logicielle, et DI vous permet de créer du code faiblement couplé qui est extrêmement flexible et facile à modifier.



Conteneurs d'injection de dépendance



De nombreux frameworks d'injection DI que vous pouvez simplement télécharger et utiliser vont plus loin et utilisent un conteneur pour l'injection de dépendances. En substance, il s'agit d'une classe qui stocke les mappages de types et retourne une implémentation enregistrée pour un type donné. Dans notre exemple simple, nous pourrons interroger le conteneur pour une instance ILogger



, et il nous retournera l'instance TextFileLogger



, ou toute instance avec laquelle nous avons initialisé le conteneur.



Dans ce cas, nous avons l'avantage de pouvoir enregistrer tous les mappages de type au même endroit, généralement là où se produit l'événement de lancement de l'application, ce qui nous permettra de voir rapidement et clairement quelles dépendances nous avons dans le système. De plus, dans de nombreux frameworks professionnels, vous pouvez configurer la durée de vie de ces objets, soit en créant de nouvelles instances à chaque nouvelle requête, soit en réutilisant une instance dans plusieurs appels.



Le conteneur est généralement créé de manière à ce que nous puissions accéder au «résolveur» (le type d'entité qui nous permet de demander des instances) de n'importe où dans le projet.

Enfin, les cadres professionnels soutiennent généralement le phénomène des sous- dépendances.- dans ce cas, la dépendance elle-même a une ou plusieurs dépendances sur d'autres types, également connues du conteneur. Dans ce cas, le résolveur peut également remplir ces dépendances, vous donnant une chaîne complète de dépendances correctement créées qui correspondent à vos mappages de types.



Créons nous-mêmes un conteneur DI très simple pour voir comment tout cela fonctionne. Une telle implémentation ne prend pas en charge les dépendances imbriquées, mais elle vous permet de mapper une interface à une implémentation, et de demander ultérieurement cette implémentation elle-même:



public class SimpleDIContainer {  
 Dictionary < Type, object > _map;
 public SimpleDIContainer() {
   _map = new Dictionary < Type, object > ();
  } 

/// <summary> 
///       ,    . 
/// </summary> 
/// <typeparam name="TIn">The interface type</typeparam> 
/// <typeparam name="TOut">The implementation type</typeparam> 
/// <param name="args">Optional arguments for the creation of the implementation type.</param> 
 public void Map <TIn, TOut> (params object[] args) {
   if (!_map.ContainsKey(typeof(TIn))) {
    object instance = Activator.CreateInstance(typeof(TOut), args);
    _map[typeof(TIn)] = instance;
   }
  } 

/// <summary> 
///  ,  T 
/// </summary> 
/// <typeparam name="T">The interface type</typeparam>
 public T GetService<T> () where T: class {
  if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
  else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
 }
}
      
      





Ensuite, nous pouvons écrire un petit programme qui crée un conteneur, affiche les types, puis demande un service. Encore une fois, un exemple simple et compact, mais imaginez à quoi cela ressemblerait dans une application beaucoup plus grande:



public class SimpleDIContainer {  
 Dictionary <Type, object> _map;
 public SimpleDIContainer() {
   _map = new Dictionary < Type, object > ();
  } 

 /// <summary> 
 ///       ,    . 
/// </summary> 
/// <typeparam name="TIn">The interface type</typeparam> 
/// <typeparam name="TOut">The implementation type</typeparam> 
/// <param name="args">Optional arguments for the creation of the implementation type.</param> 
public void Map <TIn, TOut> (params object[] args) {  
   if (!_map.ContainsKey(typeof(TIn))) {
    object instance = Activator.CreateInstance(typeof(TOut), args);
    _map[typeof(TIn)] = instance;
   }
  } 

/// <summary> 
///  ,  T 
/// </summary> 
/// <typeparam name="T">The interface type</typeparam>
 public T GetService <T> () where T: class {
  if (_map.ContainsKey(typeof(T))) return _map[typeof(T)] as T;
  else throw new ApplicationException("The type " + typeof(T).FullName + " is not registered in the container");
 }
}
      
      





Je recommande de s'en tenir à ce modèle lors de l'ajout de nouvelles dépendances à votre projet. Au fur et à mesure que votre projet grandit, vous verrez par vous-même à quel point il est facile de gérer des composants faiblement couplés. Une flexibilité considérable est gagnée et le projet lui-même est finalement beaucoup plus facile à maintenir, à modifier et à s'adapter aux nouvelles conditions.



All Articles