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