Injeção de dependência para iniciantes

Olá, Habr!



Estamos nos preparando para lançar a segunda edição do lendário livro de Mark Siman " Dependency Injection on the .NET Platform ".







Mesmo em um livro tão volumoso, dificilmente é possível cobrir totalmente esse tópico. Mas oferecemos a você uma tradução abreviada de um artigo muito acessível que descreve a essência da injeção de dependência em linguagem simples - com exemplos em C #.



O objetivo deste artigo é explicar o conceito de injeção de dependência e mostrar como ela é programada em um determinado projeto. Da Wikipedia:



A injeção de dependência é um padrão de design que separa o comportamento da resolução de dependência. Assim, é possível destacar componentes altamente dependentes uns dos outros.


Dependency Injection (ou DI) permite que você forneça implementações e serviços para outras classes para consumo; o código permanece muito fracamente acoplado. O ponto principal neste caso é o seguinte: no lugar das implementações, você pode facilmente substituir outras implementações e, ao mesmo tempo, terá que alterar um mínimo de código, uma vez que a implementação e o consumidor estão conectados, muito provavelmente, apenas por um contrato .



Em C #, isso significa que suas implementações de serviço devem atender aos requisitos da interface e, ao criar consumidores para seus serviços, você deve direcionar a interface , não a implementação, e exigir que a implementação seja fornecida ou implementada para você.para que você não precise criar as instâncias. Com essa abordagem, você não precisa se preocupar no nível da classe sobre como as dependências são criadas e de onde vêm; neste caso, apenas o contrato é importante.



Injeção de dependência por exemplo



Vejamos um exemplo em que o DI pode ser útil. Primeiro, vamos criar uma interface (contrato) que nos permitirá realizar alguma tarefa, por exemplo, registrar uma mensagem:



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





Observação: esta interface não descreve em nenhum lugar como uma mensagem é registrada e onde é registrada; aqui, a intenção é simplesmente passada para escrever a string em algum repositório. A seguir, vamos criar uma entidade que use essa interface. Digamos que criamos uma classe que mantém o controle de um diretório específico no disco e, assim que uma alteração é feita no diretório, ela registra a mensagem correspondente:



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





Nesse caso, o mais importante a notar é que nos é fornecido o construtor de que precisamos, que o implementa ILogger



. Mas, novamente, observe: não nos importamos para onde vai o log ou como é criado. Podemos apenas programar com a interface em mente e não pensar em mais nada.



Assim, para criar uma instância nossa DirectoryWatcher



, também precisamos de uma implementação pronta ILogger



. Vamos continuar e criar uma instância que registra mensagens em um arquivo 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();
  }
 }
}
      
      





Vamos criar outro que grava mensagens no log de eventos do 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);
 }
}
      
      





Agora temos duas implementações separadas que registram mensagens de maneiras muito diferentes, mas ambas o fazem ILogger



, o que significa que qualquer uma pode ser usada sempre que uma instância for necessária ILogger



. Em seguida, você pode criar uma instância DirectoryWatcher



e dizer a ela para usar um de nossos registradores:



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





Ou, simplesmente mudando o lado direito da primeira linha, podemos usar uma implementação diferente:



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





Tudo isso acontece sem nenhuma alteração na implementação do DirectoryWatcher, e isso é o mais importante. Injetamos nossa implementação de log no consumidor para que o consumidor não tenha que criar uma instância por conta própria. O exemplo mostrado é trivial, mas imagine como seria usar essas técnicas em um projeto de grande escala em que você tem várias dependências e muitas vezes mais consumidores as usam. E então, de repente, há uma solicitação para alterar o método que registra as mensagens (por exemplo, agora as mensagens devem ser registradas no servidor SQL para fins de auditoria). Se você não usar injeção de dependência de uma forma ou de outra, terá que revisar cuidadosamente o código e fazer alterações onde quer que o logger seja realmente criado e usado. Em um projeto grande, esse trabalho pode ser complicado e sujeito a erros.Com o DI, você apenas altera a dependência em um local, e o restante do aplicativo irá absorver as alterações e começar imediatamente a usar o novo método de registro.



Em essência, ele resolve o problema clássico de dependência de software e o DI permite que você crie um código fracamente acoplado que é extremamente flexível e fácil de modificar.



Recipientes de injeção de dependência



Muitos frameworks de injeção de DI que você pode simplesmente baixar e usar vão um passo adiante e usam um contêiner para injeção de dependência. Em essência, é uma classe que armazena mapeamentos de tipo e retorna uma implementação registrada para um determinado tipo. Em nosso exemplo simples, seremos capazes de consultar o contêiner por uma instância ILogger



e ele nos retornará a instância TextFileLogger



, ou qualquer instância com a qual inicializamos o contêiner.



Nesse caso, temos a vantagem de poder registrar todos os mapeamentos de tipo em um só lugar, geralmente onde ocorre o evento de lançamento do aplicativo, e isso nos permitirá ver de forma rápida e clara quais dependências temos no sistema. Além disso, em muitos frameworks profissionais, você pode configurar o tempo de vida de tais objetos, criando novas instâncias com cada nova solicitação ou reutilizando uma instância em várias chamadas.



O contêiner é geralmente criado de forma que possamos acessar o 'resolvedor' (o tipo de entidade que nos permite solicitar instâncias) de qualquer lugar no projeto.

Finalmente, as estruturas profissionais geralmente apóiam o fenômeno das subdependências.- neste caso, a própria dependência tem uma ou mais dependências de outros tipos, também conhecidos pelo contêiner. Nesse caso, o resolvedor também pode cumprir essas dependências, devolvendo a você uma cadeia completa de dependências criadas corretamente que correspondem aos seus mapeamentos de tipo.



Vamos criar um contêiner de DI muito simples para ver como tudo funciona. Essa implementação não oferece suporte a dependências aninhadas, mas permite que você mapeie uma interface para uma implementação e, posteriormente, solicite a própria implementação:



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





Em seguida, podemos escrever um pequeno programa que cria um contêiner, exibe os tipos e, em seguida, solicita um serviço. Novamente, um exemplo simples e compacto, mas imagine como seria em um aplicativo muito maior:



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





Eu recomendo manter este padrão ao adicionar novas dependências ao seu projeto. Conforme seu projeto cresce em tamanho, você verá por si mesmo como é fácil gerenciar componentes fracamente acoplados. Ganha-se uma flexibilidade considerável e o projeto em si é, em última análise, muito mais fácil de manter, modificar e se adaptar às novas condições.



All Articles