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.
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.
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.
</IServico>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
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.