Inyecci贸n de dependencia para principiantes

隆Hola, Habr!



Nos estamos preparando para lanzar la segunda edici贸n del legendario libro de Mark Siman " Dependency Injection on the .NET Platform ".







Incluso en un libro tan voluminoso, dif铆cilmente es posible cubrir completamente un tema de este tipo. Pero le ofrecemos una traducci贸n abreviada de un art铆culo muy accesible que describe la esencia de la inyecci贸n de dependencia en un lenguaje simple, con ejemplos en C #.



El prop贸sito de este art铆culo es explicar el concepto de inyecci贸n de dependencia y mostrar c贸mo se programa en un proyecto determinado. De Wikipedia:



La inyecci贸n de dependencias es un patr贸n de dise帽o que separa el comportamiento de la resoluci贸n de dependencias. Por tanto, es posible separar componentes que dependen en gran medida unos de otros.


La inyecci贸n de dependencia (o DI) le permite proporcionar implementaciones y servicios a otras clases para su consumo; el c贸digo permanece muy d茅bilmente acoplado. El punto principal en este caso es este: en lugar de implementaciones, puede sustituir f谩cilmente otras implementaciones y, al mismo tiempo, debe cambiar un m铆nimo de c贸digo, ya que la implementaci贸n y el consumidor probablemente est茅n conectados solo por un contrato .



En C #, esto significa que sus implementaciones de servicios deben cumplir con los requisitos de la interfaz, y cuando la creaci贸n de los consumidores por sus servicios, debe dirigirse a la interfaz , no la implementaci贸n, y requieren la aplicaci贸n que debe facilitarse o implementado para usted.para que no tenga que crear las instancias usted mismo. Con este enfoque, no tiene que preocuparse a nivel de clase acerca de c贸mo se crean las dependencias y de d贸nde vienen; en este caso, solo el contrato es importante.



Inyecci贸n de dependencia por ejemplo



Veamos un ejemplo en el que DI puede resultar 煤til. Primero, creemos una interfaz (contrato) que nos permitir谩 realizar alguna tarea, por ejemplo, registrar un mensaje:



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





Tenga en cuenta: esta interfaz no describe en ninguna parte c贸mo se registra un mensaje y d贸nde se registra; aqu铆 simplemente se pasa la intenci贸n de escribir la cadena en alg煤n repositorio. A continuaci贸n, creemos una entidad que utilice esta interfaz. Digamos que creamos una clase que realiza un seguimiento de un directorio espec铆fico en el disco y, tan pronto como se realiza un cambio en el directorio, registra el mensaje correspondiente:



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");
 }
}
      
      





En este caso, lo m谩s importante a tener en cuenta es que se nos proporciona el constructor que necesitamos, que implementa ILogger



. Pero, nuevamente, tenga en cuenta: no nos importa d贸nde va el registro o c贸mo se crea. Podemos programar con la interfaz en mente y no pensar en nada m谩s.



Por lo tanto, para crear una instancia nuestra DirectoryWatcher



, tambi茅n necesitamos una implementaci贸n lista para usar ILogger



. Sigamos adelante y creemos una instancia que registre mensajes en un archivo de texto:



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();
  }
 }
}
      
      





Creemos otro que escriba mensajes en el registro de eventos de 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);
 }
}
      
      





Ahora tenemos dos implementaciones separadas que registran mensajes de formas muy diferentes, pero ambas lo hacen ILogger



, lo que significa que cualquiera de las dos se puede usar donde se necesite una instancia ILogger



. A continuaci贸n, puede crear una instancia DirectoryWatcher



y decirle que use uno de nuestros registradores:



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





O, simplemente cambiando el lado derecho de la primera l铆nea, podemos usar una implementaci贸n diferente:



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





Todo esto sucede sin ning煤n cambio en la implementaci贸n de DirectoryWatcher, y esto es lo m谩s importante. Estamos inyectando nuestra implementaci贸n de registrador en el consumidor para que el consumidor no tenga que crear una instancia por su cuenta. El ejemplo que se muestra es trivial, pero imagine c贸mo ser铆a usar estas t茅cnicas en un proyecto a gran escala donde tiene m煤ltiples dependencias y hay muchas veces m谩s consumidores us谩ndolas. Y luego, de repente, hay una solicitud para cambiar el m茅todo que registra los mensajes (digamos, ahora los mensajes deben registrarse en el servidor SQL para fines de auditor铆a). Si no usa la inyecci贸n de dependencia de una forma u otra, tendr谩 que revisar cuidadosamente el c贸digo y realizar cambios donde sea que se cree y luego se use el registrador. En un proyecto grande, dicho trabajo puede resultar engorroso y propenso a errores.Con DI, simplemente cambia la dependencia en un lugar, y el resto de la aplicaci贸n realmente absorber谩 los cambios y comenzar谩 a usar inmediatamente el nuevo m茅todo de registro.



En esencia, resuelve el cl谩sico problema de software de dependencia pesada, y DI le permite crear c贸digo poco acoplado que es extremadamente flexible y f谩cil de modificar.



Envases de inyecci贸n de dependencia



Muchos marcos de inyecci贸n de DI que puede descargar y usar simplemente van un paso m谩s all谩 y usan un contenedor para la inyecci贸n de dependencia. En esencia, es una clase que almacena asignaciones de tipos y devuelve una implementaci贸n registrada para un tipo determinado. En nuestro ejemplo simple, podremos solicitar una instancia del contenedor ILogger



, y nos devolver谩 la instancia TextFileLogger



, o cualquier otra instancia con la que inicializamos el contenedor.



En este caso, tenemos la ventaja de que podemos registrar todo tipo de mapeos en un solo lugar, generalmente donde ocurre el evento de lanzamiento de la aplicaci贸n, y esto nos permitir谩 ver r谩pida y claramente qu茅 dependencias tenemos en el sistema. Adem谩s, en muchos marcos profesionales, puede configurar la vida 煤til de dichos objetos, ya sea creando instancias nuevas con cada nueva solicitud o reutilizando una instancia en varias llamadas.



El contenedor generalmente se crea de tal manera que podemos acceder al 'resolutor' (el tipo de entidad que nos permite solicitar instancias) desde cualquier lugar del proyecto.

Finalmente, los marcos profesionales suelen apoyar el fen贸meno de las subdependencias.- en este caso, la propia dependencia tiene una o m谩s dependencias de otros tipos, tambi茅n conocidos por el contenedor. En este caso, el solucionador tambi茅n puede cumplir con esas dependencias, devolvi茅ndole una cadena completa de dependencias creadas correctamente que corresponden a sus asignaciones de tipos.



Creemos nosotros mismos un contenedor DI muy simple para ver c贸mo funciona todo. Dicha implementaci贸n no admite dependencias anidadas, pero le permite asignar una interfaz a una implementaci贸n y luego solicitar la implementaci贸n en s铆:



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");
 }
}
      
      





A continuaci贸n, podemos escribir un peque帽o programa que crea un contenedor, muestra los tipos y luego solicita un servicio. Nuevamente, un ejemplo simple y compacto, pero imagine c贸mo se ver铆a en una aplicaci贸n mucho m谩s 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");
 }
}
      
      





Recomiendo seguir este patr贸n al agregar nuevas dependencias a su proyecto. A medida que su proyecto crezca en tama帽o, ver谩 por s铆 mismo lo f谩cil que es administrar componentes poco acoplados. Se gana una flexibilidad considerable y, en 煤ltima instancia, el proyecto en s铆 es mucho m谩s f谩cil de mantener, modificar y adaptar a las nuevas condiciones.



All Articles