Dependency Injection
Dependency Injection (injeção de dependência), ou simplesmente DI, é a técnica que implementa o princípio de inversão de controle (IoC), que veremos mais adiante neste artigo. Ao longo do texto, vamos partir de um código mal organizado e refatorá-lo passo a passo até chegar a uma aplicação desacoplada e testável.
Quando falamos em arquitetura de software, independentemente de a aplicação ser backend, frontend ou mobile, sempre buscamos algo parecido: organização.
Organizar bem as aplicações permite testes melhores, facilita encontrar bugs e, principalmente, ajuda o projeto a crescer. Afinal, queremos que a aplicação não vire um Frankenstein todo remendado quando novas funcionalidades chegarem.
Mas o que a Dependency Injection tem a ver com isso? E o que IoC, DIP e Service Locator têm em comum com ela? Vamos detalhar tudo nas próximas seções.
Um mau exemplo de código acoplado
Provavelmente você já ouviu que as aplicações devem ter baixo acoplamento entre os módulos. Mas o que isso significa na prática?
Para responder, vou usar como base um código do curso 7182, Refatorando para testes de unidade. O código é extenso, mas dê uma boa olhada nele.
public class OrderController : Controller
{
[Route("v1/orders")]
[HttpPost]
public async Task<string> Place(string customerId, string zipCode, string promoCode, int[] products)
{
// #1 - Recupera o cliente
Customer customer = null;
using (var conn = new SqlConnection("CONN_STRING"))
{
customer = conn.Query<Customer>
("SELECT * FROM CUSTOMER WHERE ID=" + customerId)
.FirstOrDefault();
}
// #2 - Calcula o frete
decimal deliveryFee = 0;
var request = new HttpRequestMessage(HttpMethod.Get, "URL/" + zipCode);
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
using (HttpClient client = new HttpClient())
{
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
deliveryFee = await response.Content.ReadAsAsync<decimal>();
}
else
{
// Caso não consiga obter a taxa de entrega, o valor padrão é 5
deliveryFee = 5;
}
}
// #3 - Calcula o total dos produtos
decimal subTotal = 0;
for (int p = 0; p < products.Length; p++)
{
var product = new Product();
using (var conn = new SqlConnection("CONN_STRING"))
{
product = conn.Query<Product>
("SELECT * FROM PRODUCT WHERE ID=" + products[p])
.FirstOrDefault();
}
subTotal += product.Price;
}
// #4 - Aplica o cupom de desconto
decimal discount = 0;
using (var conn = new SqlConnection("CONN_STRING"))
{
var promo = conn.Query<PromoCode>
("SELECT * FROM PROMO_CODES WHERE CODE=" + promoCode)
.FirstOrDefault();
if (promo.ExpireDate > DateTime.Now)
{
discount = promo.Value;
}
}
// #5 - Gera o pedido
var order = new Order();
order.Code = Guid.NewGuid().ToString().ToUpper().Substring(0, 8);
order.Date = DateTime.Now;
order.DeliveryFee = deliveryFee;
order.Discount = discount;
order.Products = products;
order.SubTotal = subTotal;
// #6 - Calcula o total
order.Total = subTotal - discount + deliveryFee;
// #7 - Retorna
return $"Order {order.Code} placed successfully!";
}
}
Você deve ter notado, e talvez até se incomodado, com a quantidade de coisas que essa ação do controlador realiza. Isso claramente fere o S do SOLID, o Single Responsibility Principle (princípio da responsabilidade única).
O problema acontece porque o mesmo método concentra diversas responsabilidades que poderiam, e deveriam, estar fora dele:
- Conexão com banco de dados.
- Requisição para uma API externa.
- Cálculo de regras de negócio.
Como melhorar esse código? Talvez você até saiba o que precisa ser feito, mas ainda não saiba como fazer.
Por que separar responsabilidades
Precisamos de fato separar as coisas e manter a conexão com o banco fora daquele método, por exemplo. Afinal, diversos outros métodos também usam conexão com o banco.
Basicamente, não queremos escrever o mesmo código em dois ou mais lugares. Sempre que isso acontece, vale a pena extrair o código para um ponto único e apenas chamá-lo.
Com isso, também definimos o SPOF (Single Point of Failure), ou ponto único de falha. Dessa forma, quando houver algum problema na conexão com o banco, sabemos que ele estará naquela classe, porque o código só existe lá e os demais lugares apenas o usam.
Se quiser se aprofundar no assunto em vídeo, conheça a masterclass de injeção de dependência do balta.io.
Primeiro passo rumo à Dependency Injection
Claro que a Dependency Injection não é a solução para tudo, mas fica mais fácil explicá-la quando estamos alinhados sobre o problema. Então, partindo desse código, o que você mudaria e como?
Vamos dar o primeiro passo e separar, por exemplo, a chamada à API de cálculo de frete.
public class DeliveryService
{
public async Task<decimal> GetDeliveryFeeAsync(string zipCode)
{
var request = new HttpRequestMessage(HttpMethod.Get, "URL/" + zipCode);
request.Headers.Add("Accept", "application/json");
request.Headers.Add("User-Agent", "HttpClientFactory-Sample");
using (var client = new HttpClient())
{
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
return await response.Content.ReadAsAsync<decimal>();
// Valor padrão quando a API de frete falha
return 5;
}
}
}
Nesse caso, criamos a classe DeliveryService com o método GetDeliveryFeeAsync, que obtém o valor do frete de acordo com o CEP do usuário. Agora podemos chamar esse serviço no controlador.
public class OrderController : Controller
{
[Route("v1/orders")]
[HttpPost]
public async Task<string> Place(string customerId, string zipCode, string promoCode, int[] products)
{
// ...
var deliveryService = new DeliveryService();
var deliveryFee = await deliveryService.GetDeliveryFeeAsync(zipCode);
// ...
}
}
Embora a refatoração tenha permitido reaproveitar o cálculo do frete, o controlador continua fortemente acoplado a ele. Repare que o próprio controlador cria o serviço com new.
Sabemos que os testes de unidade não podem depender de serviços externos. Então, como você testaria esse método?
O que é Dependency Injection
Dependency Injection, ou injeção de dependência, é a técnica que implementa o padrão IoC. Em outras palavras, um objeto recebe de fora as dependências de que precisa, em vez de criá-las por conta própria.
O ideal nesse cenário é que o controlador dependa do serviço de cálculo de frete, mas não seja responsável por gerenciá-lo.
Pense no seu primeiro dia de trabalho em uma empresa. Você precisa de um computador para trabalhar, mas quem vai fornecê-lo e como ele chega até você não é responsabilidade sua, ou pelo menos não deveria ser.
Voltamos ao S do SOLID. Qual é a responsabilidade da ação de realizar um pedido? Realizar um pedido, e nada além disso.
Para realizar um pedido, porém, precisamos calcular o frete. E para calcular o frete, precisamos do serviço de cálculo de fretes.
Notou como o código criou dependências? Percebeu que, quanto mais separamos as responsabilidades, mais evidente fica o uso de dependências?
Formas de aplicar a Dependency Injection
Existem três formas clássicas de entregar uma dependência a um objeto. A mais comum é a injeção pelo construtor, em que a classe declara no construtor tudo o que precisa.
Também existe a injeção por propriedade, em que a dependência é atribuída a uma propriedade pública depois da criação do objeto. Por fim, há a injeção por método, em que a dependência chega como parâmetro do método que vai usá-la.
No ASP.NET Core, a injeção pelo construtor é o padrão recomendado. Além disso, as ações dos controladores aceitam parâmetros marcados com o atributo [FromServices], que funciona como uma injeção por método.
IoC, a inversão de controle
Vamos dar mais um passo e refatorar o código, explicitando a dependência do serviço. Faremos isso com base em um princípio chamado IoC (Inversion of Control), ou inversão de controle.
No IoC, externalizamos as responsabilidades invertendo o controle. Ou seja, em vez de o controlador controlar o serviço de frete, ele apenas depende dele.
Podemos notar o uso do IoC sempre que vemos classes ou interfaces sendo recebidas no construtor.
public class OrderController : Controller
{
private readonly DeliveryService _deliveryService;
public OrderController(DeliveryService deliveryService)
{
_deliveryService = deliveryService;
}
[Route("v1/orders")]
[HttpPost]
public async Task<string> Place(string customerId, string zipCode, string promoCode, int[] products)
{
// ...
var deliveryFee = await _deliveryService.GetDeliveryFeeAsync(zipCode);
// ...
}
}
O que fizemos foi mover o DeliveryService para o construtor do controlador, criando uma dependência explícita e preparando o terreno para a Dependency Injection.
A partir de agora, toda vez que alguém instanciar o controlador, será obrigatório informar um serviço de frete. Como esse serviço será fornecido e de onde ele vem não é mais responsabilidade do controlador, então resolveremos isso depois.
O código deu um passo adiante e, como você pode imaginar, dependências vão surgir o tempo todo. Em uma aplicação simples, por exemplo, o controlador depende de um serviço, que depende de um repositório, que depende de um contexto de dados, que depende de uma SqlConnection.
Para entender esse princípio com mais calma, leia também o artigo sobre Inversion of Control.
Testando o controlador
Agora vamos ver se a Dependency Injection já ajuda nos testes de unidade do método Place.
[TestMethod]
public void ShouldPlaceAnOrder()
{
var service = new DeliveryService();
var controller = new OrderController(service);
// ...
}
Opa, espere um minuto. Existe uma premissa nos testes de unidade: eles não devem depender de itens externos, como bancos de dados e APIs.
A publicação do código provavelmente será automatizada, o que significa que um job do GitHub, por exemplo, fará o build, o publish e o deploy. Durante esse processo, temos a etapa de CI (Continuous Integration) do DevOps, em que executamos todos os testes de unidade para verificar se nada quebrou.
Os testes de unidade servem justamente para garantir a integridade da aplicação. Portanto, se eles falharem, a aplicação não será publicada.
Agora, o que acontece se a API de cálculo de frete estiver fora do ar durante a execução dos testes? E se os dados do banco de origem forem diferentes dos dados locais e os testes falharem por isso?
Além disso, em ambientes maiores, o tempo de deploy é importante, e depender de serviços externos afeta muito esse tempo. Imagine o deploy e a execução desses testes em 20 ou 30 máquinas ao mesmo tempo.
Por esses e outros motivos, não devemos depender de itens externos nos testes, e a Dependency Injection sozinha ainda não resolve isso. Se quiser revisar esse conceito, o artigo sobre fundamentos dos testes de unidade aprofunda o assunto.
DIP, o complemento da Dependency Injection
Você deve estar se perguntando como resolver esse problema. A resposta está novamente no SOLID, mais especificamente na letra D.
O DIP (Dependency Inversion Principle), ou princípio da inversão de dependência, prega que devemos depender de abstrações e não de implementações.
Abstrações são contratos, cascas que escondem os detalhes da execução. Por exemplo, enquanto escrevo, estou sentado na concessionária esperando a revisão do meu carro.
Nesse caso, a concessionária é a abstração da mecânica automotiva para mim. Eu não sei o que acontece lá dentro nem o que precisa ser feito, então apenas levo o carro e ele volta revisado.
O DIP é um princípio de design que complementa o IoC e a Dependency Injection. Vamos implementá-lo, e o primeiro passo é criar uma abstração do serviço.
public interface IDeliveryService
{
Task<decimal> GetDeliveryFeeAsync(string zipCode);
}
Criamos um contrato, uma interface que diz apenas o que um serviço de entrega precisa oferecer, mas não como ele faz isso. Em seguida, refatoramos o serviço para implementar a interface IDeliveryService.
public class DeliveryService : IDeliveryService
{
public async Task<decimal> GetDeliveryFeeAsync(string zipCode)
{
// ...
}
}
Esse procedimento ainda não muda nada no controlador nem nos testes. Por isso, precisamos refatorá-los para depender da abstração e não da implementação. O artigo sobre Dependency Inversion Principle explora esse princípio em detalhes.
Refatorando o controlador
Agora que temos a interface, o construtor do controlador pode depender dela em vez da classe concreta, aplicando o DIP.
public class OrderController : Controller
{
private readonly IDeliveryService _deliveryService;
public OrderController(IDeliveryService deliveryService)
{
_deliveryService = deliveryService;
}
// ...
}
Parece uma mudança simples e sem importância, correto? Mas veja o impacto dela nos testes.
Mockando o serviço
Se não devemos depender de serviços externos e o controlador depende de um serviço de frete, como resolver o problema?
A saída é criar um serviço falso, que simula a ida à API externa. Chamamos esses objetos de mocks e, embora existam frameworks para isso, vamos criar um manualmente aqui.
public class FakeDeliveryService : IDeliveryService
{
public Task<decimal> GetDeliveryFeeAsync(string zipCode)
{
// Sempre retorna um frete fixo para o teste
return Task.FromResult(10m);
}
}
Nesse caso, sempre retornamos o valor 10, mas você pode usar a imaginação para atender às necessidades de cada teste.
Refatorando os testes
Como o FakeDeliveryService também é um IDeliveryService e o controlador depende da abstração, podemos injetá-lo no controlador.
[TestMethod]
public void ShouldPlaceAnOrder()
{
IDeliveryService service = new FakeDeliveryService();
var controller = new OrderController(service);
// ...
}
Dessa forma, não só desacoplamos o serviço de cálculo de frete do controlador, como também permitimos que ele seja substituído por um mock. Com isso, o controlador se torna testável.
Entende como a Dependency Injection é apenas a ponta de um iceberg?
Service Locator e containers de IoC
Nosso trabalho ainda não terminou. Na verdade, geramos mais trabalho, mas a boa notícia é que vamos automatizá-lo.
Todas as dependências que criamos, e no mundo real elas não serão poucas, precisam ser resolvidas em algum momento. Toda vez que dizemos "isso não é problema meu", o problema não deixa de existir, apenas muda de lugar.
Convenhamos que resolver todas essas dependências manualmente seria um suicídio. Imagine surgir uma nova implementação de um contrato: a refatoração seria imensa.
Para isso existe o Service Locator, um padrão que ajuda a descobrir a implementação de cada interface e a instanciá-la. Suas implementações costumam aparecer com o nome de containers de IoC, e algumas das mais conhecidas são o Unity e o Ninject.
Vale um cuidado, porém. Quando uma classe pede dependências diretamente ao container, o Service Locator esconde as dependências reais e dificulta os testes, por isso muitos autores o consideram um antipadrão nesse uso.
Na prática, o ideal é deixar o container trabalhar nos bastidores e manter a Dependency Injection pelo construtor nas suas classes.
Dependency Injection no ASP.NET Core
O ASP.NET Core traz uma implementação nativa que resolve as dependências e, sinceramente, atende muito bem à maioria dos cenários. Particularmente, faz um bom tempo que não uso um pacote externo para isso.
Para registrar as dependências com o container padrão, basta chamar AddTransient, AddScoped ou AddSingleton no método ConfigureServices da classe Startup.
public class Startup
{
// ...
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddTransient<IDeliveryService, DeliveryService>();
// ...
}
// ...
}
A partir desse registro, sempre que o ASP.NET Core criar o OrderController, ele perceberá que o construtor pede um IDeliveryService e entregará uma instância de DeliveryService automaticamente.
Tempos de vida dos serviços
Cada método de registro define um tempo de vida diferente. Segundo a documentação oficial de Dependency Injection no ASP.NET Core, o container cria um serviço Transient a cada vez que alguém o solicita.
Já um serviço Scoped nasce uma vez por requisição e é descartado ao fim dela. Por isso, o DbContext do Entity Framework Core usa esse tempo de vida por padrão.
Por fim, um serviço Singleton é criado uma única vez e compartilhado durante toda a vida da aplicação.
Nunca injete um serviço Scoped dentro de um Singleton, porque ele passaria a se comportar como singleton e poderia carregar estado incorreto entre requisições. No ambiente de desenvolvimento, o próprio container valida esse cenário e lança uma exceção.
O artigo AddTransient, AddScoped e AddSingleton compara esses três tempos de vida com exemplos práticos.
Benefícios da Dependency Injection
Ao final dessa jornada, o ganho fica claro. Primeiro, as classes passam a ter responsabilidades bem definidas e dependem apenas de contratos.
Em seguida, os testes de unidade ficam rápidos e independentes de bancos de dados ou APIs, porque basta injetar um mock. Além disso, trocar uma implementação, como mudar o provedor de frete, exige apenas alterar um registro no Startup.
Para saber mais detalhes sobre o container nativo, veja este artigo sobre DI no ASP.NET Core que publiquei há algum tempo.
Conclusão
A Dependency Injection é a técnica que tira das classes a responsabilidade de criar suas próprias dependências. Combinada ao IoC, ao DIP e a um container, ela transforma um controlador cheio de responsabilidades em um código organizado, desacoplado e testável.
Neste artigo, partimos de um método que acessava banco, API e regras de negócio ao mesmo tempo. Depois, extraímos um serviço, invertemos o controle, criamos uma abstração, escrevemos um mock e, por fim, deixamos o ASP.NET Core resolver tudo com a Dependency Injection nativa.
Agora é a sua vez: escolha um controlador do seu projeto e aplique esses passos para sentir a diferença.
FAQ
Qual a diferença entre Dependency Injection e IoC?
IoC é o princípio de inverter o controle da criação das dependências. A Dependency Injection é a técnica que coloca esse princípio em prática, entregando as dependências de fora para dentro.
Dependency Injection é a mesma coisa que DIP?
Não. O DIP diz para depender de abstrações, enquanto a Dependency Injection define como as dependências chegam até a classe.
Preciso de um pacote externo para usar Dependency Injection no ASP.NET Core?
Não. O ASP.NET Core já traz um container nativo que atende à maioria dos cenários.
Quando usar AddTransient, AddScoped ou AddSingleton?
Use Transient para serviços leves e sem estado, Scoped para serviços ligados à requisição e Singleton para objetos compartilhados por toda a aplicação.
Por que a Dependency Injection facilita os testes de unidade?
Porque a classe depende de uma abstração, então o teste pode injetar um mock no lugar do serviço real.