A injeção de dependência (DI) é um importante padrão de projeto que implementa o baixo acoplamento entre os diversos módulos de um projeto. Por exemplo, na Figura 1, quando a classe A utiliza funcionalidades da classe B, pode-se dizer que a classe A possui dependência da classe B. A dependência é então qualquer objeto exigido por outro objeto.

Exemplo de dependência
Figura 1. Exemplo de dependência

A DI realiza a associação representada acima, entre o tipo solicitado pelo cliente (a interface é a mais comum) e o tipo do retorno. Neste caso, não é o cliente que determina o que será instanciado, mas sim a DI que determina o retorno. Desta forma, a DI fornece uma instância do serviço (e não o cliente que instancia diretamente).

Por que utilizar?

Dentre as principais motivações de utilizar os princípios da DI (Dependecy Injection) estão:

  • Evitar problemas com multi-threading;
  • Evitar potenciais bugs;
  • Evitar falha de memória;
  • Design de serviços e suas dependências.

Quando instanciamos um objeto no .NET com chamada para o construtor, cria-se uma conexão acoplada do aplicativo com o objeto instanciado. Em alguns serviços, por exemplo Logon (utilizando Log4Net e NLog para clientes diferentes), a dependência pode não funcionar adequadamente, ao referenciar ambos os serviços de logon.

Utilizando DI, o aplicativo solicita uma coleção de serviços para a instância, ao invés de solicitar o serviço diretamente com o operador. Esta solicitação não é de um tipo específico (para evitar acoplamento), mas sim, a uma interface como ILoggerFactory para que o provedor de serviço (Log4Net ou NLog) a implemente.

Na prática

Colocando em prática, vamos criar um projeto MVC com uma definição de interface e uma implementação. Para isso, adicione dois novos itens ao projeto criado:

  • Interface – utilize o nome ILab01;
  • Classe – utilize o nome Lab01A.

A interface foi criada com a declaração de uma mensagem inicial:

public interface ILab01
       {
           string MsgInicial();
       }

E a classe de implementação com o retorno, referenciando a interface criada anteriormente:

public class Lab01A : ILab01
      {
          public string MsgInicial()
          {
              return $"Primeira mensagem {nameof(Lab01A)}";
          }

      }

Em seguida, registre Lab01A no container do DI, acessando o arquivo Startup.cs:

 
public void ConfigureServices(IServiceCollection services)
{
	services.AddTransient<ILab01, Lab01A>();
}

Após o registro da implementação da interface, acesse a pasta “Controllers” (arquivo HomeController.cs) e adicione o construtor da injeção em HomeController : Controller.

public ILab01 Lab01 { get; set; }

public HomeController(ILab01 Lab)
{
    Lab01 = Lab;
}

public IActionResult Index()
{
    var mensagem = Lab01.MsgInicial();
    return Content(mensagem);

}

Como estamos trabalhando com uma interface, o resultado é o item registrado no DI container. Para múltiplas implementações, pode-se utilizar outros recursos para ordenar a exibição dos itens no runtime.

Primeira mensagem
Figura 2. Primeira mensagem

E quais são as vantagens e desvantagens de utilizar injeção de dependências?

Vantagens

  • Classes mais modulares, pois dependem apenas da Interface de dependências passadas;
  • Facilita o teste em partes isoladas e a reorganização de partes genéricas em novas aplicações;
  • Reutilização e manutenção do código;
  • Ajuda em teste unitário e a obter menor acoplamento.

Desvantagens

    Desvantagens
  • Muitos erros em tempo de compilação são enviados para runtime;
  • O uso em excesso pode levar a problemas de gerenciamento, entre outros;
  • A implementação com reflexão ou programação dinâmica pode impedir o uso da automação do IDE.

Padrões de DI (dependecy injection)

Constructor Injection

É um padrão de DI utilizado para declarar e obter dependências de um serviço por meio do construtor do serviço. O uso é recomendado quando a dependência for obrigatória, garantindo assim a declaração da dependência na definição da classe.

Assim, a dependência estará pronta para o uso durante todo o ciclo de vida do objeto que a consome. No exemplo abaixo, VendaIngresso injeta IEventoDisponibilidade como dependência no construtor e o utiliza no método Delete.

public class VendaIngresso
        {
            private readonly IEventoDisponibilidade _eventoDisponibilidade;

            public VendaIngresso(IEventoDisponibilidade eventoDisponibilidade)
            {
                _eventoDisponibilidade = eventoDisponibilidade;
            }

            public void Delete (int id)
            {
                _eventoDisponibilidade.Delete(id);
            }
        }

Boas práticas

  • Definir as dependências requeridas de forma explícita no serviço construtor, assim o serviço não pode ser construído sem as dependências;
  • Atribuir a injected dependency como somente leitura a fim de evitar atribuições indevidas a ele dentro de um método.

Property Injection

Property injection utiliza propriedades de escrita, ao invés de parâmetros de construtor para executar a injeção. A injeção de métodos define as dependências através do método.

O container de injeção de dependência padrão do ASP.NET Core não possui suporte à injeção de propriedade. Por isso, deve ser utilizado outro container que suporte a injeção de propriedade. No exemplo abaixo, VendaIngresso está declarando uma propriedade Mensagem com setter público. O dependency injection container pode definir Mensagem se ele estiver disponível.

using Microsoft.Extensions.Logging;
       using Microsoft.Extensions.Logging.Abstractions;

       namespace AppEventos
       {
           public class VendaIngresso
           {
               public IMensagem<VendaIngresso> Mensagem { get; set; }

               private readonly IEventoDisponibilidade _eventoDisponibilidade;

               public VendaIngresso(IEventoDisponibilidade eventoDisponibilidade)
               {
                   _eventoDisponibilidade = eventoDisponibilidade;

                   Mensagem = NullLogger<VendaIngresso>.Instance;
               }

               public void Delete(int id)
               {
                   _eventoDisponibilidade.Delete(id);
                   Mensagem.LogInformation(
                       $"Evento apagado com o ID = {id}");
               }
           }
       }

Service Locator

Service Locator é outro padrão para obter dependências. Ele cria uma camada de abstração no processo, e assim, as dependências são solicitadas a partir de um objeto centralizado. No exemplo abaixo, VendaIngresso está injetando IProvedorServico e resolvendo dependências usando-o.

public class VendaIngresso
       {
           private readonly IEventoDisponibilidade _eventoDisponibilidade;

           private readonly IMensagem<VendaIngresso> _mensagem;

           public VendaIngresso(IProvedorServico provedorServico)
           {
               _eventoDisponibilidade = provedorServico
                 .GetRequiredService<IEventoDisponibilidade>();

               _mensagem = provedorServico
                 .GetService<IMensagem<VendaIngresso>>() ??
                   NullLogger<VendaIngresso>.Instance;
           }
           public void Delete(int id)
           {
               _eventoDisponibilidade.Delete(id);
               _mensagem.LogInformation($"Evento apagado com o ID = {id}");
           }
       }

Service Life Times

Há três service lifetimes no ASP.NET Core DI:

  • Transient: serviços são criados toda vez que há injeção ou requisição. É altamente recomendado por não precisar se preocupar com multi-threading e falhas de memória;
  • Scope: a criação por escopo cria um novo escopo de serviço separado a cada requisição web. Não é recomendado o uso do serviço em aplicações que não sejam web.
  • Singleton: serviços criados através do DI container. Geralmente é criado uma única vez para o ciclo de vida completo da aplicação. O uso deve considerar multi-threading e prevenir falhas de memória.

No exemplo utilizando Transient, instanciamos uma nova implementação IServico para cada chamada, aproveitando a injeção automática do construtor.

container.Register<IServico, Servico>(Lifestyle.Transient);

           ou

           container.Register<IServico, Servico>();
</IServico>

Ou iniciando uma nova instância Servico a cada chamada, utilizado delegate:

container.Register<IServico>(() => new Servico(new SqlRepository()),
           Lifestyle.Transient);

Em Scope, após criar o scoped lifestyle padrão, as demais configurações podem acessar este lifestyle através de Lifestyle.Scoped:

var container = new Container();
           container.Options.DefaultScopedLifestyle = new AsyncScopedLifestyle();

           container.Register<IUserContext, AspNetUserContext>(Lifestyle.Scoped);
           container.Register<MyAppUnitOfWork>(() => new MyAppUnitOfWork("constr"),
               Lifestyle.Scoped);

           container.RegisterInstance<IServico>(servico);

Já o Singleton pode ser registrado especificando o tipo do serviço e a implementação como argumentos de tipo genérico. Também pode utilizar o método RegisterInstance(T) para atribuir uma instância construída manualmente:

ccontainer.Register<IServico, Servico>(Lifestyle.Singleton);

       ou

       var servico = new Servico(new SqlRepository());
       container.RegisterInstance<IServico>(servico);

Veja que com os princípios apresentados conseguimos criar aplicações altamente desacopladas para incrementar a reusabilidade dos componentes de seus projetos.

Confira também