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

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

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

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:

container.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