Dependency Inversion Principle
O Dependency Inversion Principle (DIP), ou princípio da inversão de dependência, prega que devemos depender de abstrações e não de implementações. Neste artigo, você vai entender o que esse princípio significa, como ele se diferencia de DI e IoC e como aplicá-lo em C# com um exemplo prático.
O que é o Dependency Inversion Principle
O princípio da inversão de dependência diz que devemos depender de abstrações, como interfaces, em vez de implementações, como classes concretas. Com isso, reduzimos o acoplamento entre as camadas do sistema.
O Dependency Inversion Principle é a letra D do SOLID, o conjunto de cinco princípios de design orientado a objetos popularizado por Robert C. Martin. Os outros quatro são responsabilidade única, aberto e fechado, substituição de Liskov e segregação de interfaces.
Se você quiser uma visão geral dos cinco princípios antes de seguir, leia o artigo sobre SOLID aqui no balta.io.
As duas regras do princípio
Na formulação clássica, o princípio tem duas partes. A primeira diz que módulos de alto nível não devem depender de módulos de baixo nível, e que ambos devem depender de abstrações.
A segunda diz que abstrações não devem depender de detalhes. Pelo contrário, são os detalhes que devem depender das abstrações.
Na prática, módulos de alto nível são aqueles que contêm as regras de negócio, como um caso de uso de pedidos. Já os módulos de baixo nível cuidam de detalhes técnicos, como acesso a banco de dados, envio de e-mail ou chamadas HTTP.
Por que o nome fala em inversão
Na maioria das aplicações, a dependência em tempo de compilação segue a mesma direção da execução. Ou seja, se a classe A chama a classe B e B chama C, então A depende de B e B depende de C.
Segundo a documentação de princípios de arquitetura da Microsoft, aplicar o DIP faz A chamar uma abstração que B implementa. Assim, em tempo de execução o fluxo continua igual, mas em tempo de compilação B passa a depender de uma interface controlada por A.
É exatamente essa troca de direção que dá nome ao princípio. Com isso, a regra de negócio deixa de conhecer a infraestrutura e passa a ditar o contrato que a infraestrutura precisa cumprir.
Dependency Inversion Principle, DI e IoC
Embora esteja relacionado a DI (Dependency Injection) e a IoC (Inversion of Control), o DIP é diferente em diversos aspectos. Por isso, vale separar bem cada conceito.
DI é uma técnica que auxilia na implementação de outro princípio de design, chamado IoC. Em outras palavras, a injeção de dependência é a forma de entregar a uma classe os objetos de que ela precisa, em vez de deixá-la criar esses objetos sozinha.
O Dependency Inversion Principle se relaciona diretamente com DI, porque podemos usar a técnica para fornecer as dependências. Nesse caso, porém, as dependências são abstrações.
A própria Microsoft resume essa relação ao afirmar que a prática da injeção de dependência se torna possível quando seguimos o princípio da inversão de dependência. Resumindo: o DIP diz do que depender, enquanto a DI diz como a dependência chega até a classe.
Dependency Injection sem Dependency Inversion Principle
Podemos ter DI sem necessariamente aplicar o DIP, como mostra o exemplo abaixo. Nesse caso, estamos injetando uma dependência, mas ela é uma implementação concreta e não uma abstração.
Isso resolve o problema de reúso de código que surgiria se a responsabilidade de ler o cliente do banco ficasse no controlador. Mesmo assim, ainda temos um problema aqui.
public class OrderController : Controller
{
private readonly CustomerRepository _repository;
public OrderController(CustomerRepository repository)
=> _repository = repository;
[Route("v1/orders")]
[HttpPost]
public async Task<string> Place(PlaceOrderCommand command)
{
// #1 - Recupera o cliente
var customer = await _repository.GetByIdAsync(command.CustomerId);
// ...
}
}
Como dependemos de uma implementação concreta, não temos flexibilidade para usar diversas implementações desse repositório. O controlador, que é um módulo de alto nível, está preso a um detalhe de baixo nível.
Para estudar o tema com mais profundidade, conheça o curso Dominando Injeção de Dependência.
Dependency Inversion Principle e testes de unidade
Mas por que ter diversas implementações de uma mesma interface? Um cenário simples e objetivo são os testes de unidade, que não devem depender de itens externos.
No exemplo anterior, temos uma clara dependência do SQL Server, já que os clientes estão armazenados lá. Então, como executar testes sem depender do SQL Server?
A resposta são os mocks ou fakes. Ou seja, falsificamos uma implementação e fingimos que fomos ao banco e recuperamos o cliente.
Esse cenário só é possível com a aplicação do Dependency Inversion Principle. Afinal, se o controlador exige a classe concreta, não há como entregar outra coisa para ele.
Além disso, testes que acessam banco ou rede ficam lentos e instáveis, porque dependem de dados, conexões e serviços que podem mudar. Para revisar essa ideia, o artigo sobre fundamentos dos testes de unidade explica por que isolar dependências é tão importante.
Dependency Inversion Principle na prática
Aplicar o Dependency Inversion Principle é relativamente simples. Basta criar uma interface ICustomerRepository e fazer o controlador depender dela, e não da implementação concreta CustomerRepository.
public interface ICustomerRepository
{
Task<Customer?> GetByIdAsync(Guid id);
}
Em seguida, trocamos o tipo do campo e do parâmetro do construtor no controlador.
public class OrderController : Controller
{
private readonly ICustomerRepository _repository;
public OrderController(ICustomerRepository repository)
=> _repository = repository;
[Route("v1/orders")]
[HttpPost]
public async Task<string> Place(PlaceOrderCommand command)
{
// #1 - Recupera o cliente
var customer = await _repository.GetByIdAsync(command.CustomerId);
// ...
}
}
Como as assinaturas são as mesmas, o código no controlador não muda, exceto a dependência. Essa é uma das vantagens do princípio: a mudança fica concentrada em um único ponto.
Quem é dono da abstração
Um detalhe que costuma passar despercebido é onde a interface deve ficar. Pelo Dependency Inversion Principle, quem define o contrato é o módulo de alto nível, porque é ele que sabe do que precisa.
Por isso, a interface ICustomerRepository deve morar junto das regras de negócio, e não no projeto de infraestrutura. Dessa forma, a infraestrutura referencia o domínio para implementar o contrato, e nunca o contrário.
Essa organização é a base de arquiteturas como a Clean Architecture, em que as dependências apontam sempre para o centro da aplicação. O artigo sobre EF Core em Clean Architecture mostra essa estrutura aplicada a um projeto real.
Múltiplas implementações
Como vantagem, agora podemos ter múltiplas implementações da mesma abstração. No nosso caso, teremos a CustomerRepository e a FakeCustomerRepository.
A primeira acessa o SQL Server de verdade. Aqui usamos o Dapper para executar a consulta parametrizada:
public class CustomerRepository : ICustomerRepository
{
public async Task<Customer?> GetByIdAsync(Guid id)
{
using var conn = new SqlConnection("CONN_STRING");
const string query = "SELECT * FROM CUSTOMER WHERE ID=@id";
return await conn.QueryFirstOrDefaultAsync<Customer>(query, new { id });
}
}
A segunda apenas devolve um cliente fixo, sem tocar em banco algum:
public class FakeCustomerRepository : ICustomerRepository
{
public Task<Customer?> GetByIdAsync(Guid id)
{
// Simula a busca no banco sem acessar o SQL Server
return Task.FromResult<Customer?>(new Customer("André", "Baltieri"));
}
}
Perceba que as duas classes cumprem o mesmo contrato. Portanto, para o controlador, tanto faz qual delas ele recebe.
O mesmo raciocínio vale para outros cenários além dos testes. Por exemplo, você poderia criar uma implementação que lê de um cache antes de ir ao banco, ou migrar para outro banco de dados sem tocar no controlador.
Testes de unidade com o Dependency Inversion Principle
Como o controlador depende da abstração ICustomerRepository, ele aceita tanto um CustomerRepository quanto um FakeCustomerRepository. Com isso, podemos usar mocks nos testes.
[TestMethod]
public async Task ShouldPlaceAnOrder()
{
ICustomerRepository repository = new FakeCustomerRepository();
var controller = new OrderController(repository);
var result = await controller.Place(new PlaceOrderCommand());
// ...
}
Dessa forma, aplicamos o Dependency Inversion Principle e ainda temos testes livres de dependências externas. O teste roda em milissegundos, não precisa de conexão e produz sempre o mesmo resultado.
Em vez de criar a classe falsa à mão, você também pode usar bibliotecas de mock, que geram a implementação em tempo de execução. Mesmo assim, o requisito é o mesmo: a classe testada precisa depender de uma abstração.
Outro exemplo do Dependency Inversion Principle
O repositório é o exemplo mais comum, mas o princípio vale para qualquer detalhe técnico. Imagine que, depois de criar o pedido, a aplicação precise avisar o cliente.
Sem o Dependency Inversion Principle, a classe de pedidos chamaria diretamente uma classe de envio de e-mail. Assim, qualquer mudança no canal de comunicação obrigaria a alterar a regra de negócio.
Em vez disso, a camada de negócio define o contrato de que precisa:
public interface INotificationService
{
Task SendAsync(Customer customer, string message);
}
Em seguida, a infraestrutura fornece uma ou mais implementações desse contrato:
public class EmailNotificationService : INotificationService
{
public Task SendAsync(Customer customer, string message)
{
// Envia a mensagem por e-mail
return Task.CompletedTask;
}
}
public class SmsNotificationService : INotificationService
{
public Task SendAsync(Customer customer, string message)
{
// Envia a mensagem por SMS
return Task.CompletedTask;
}
}
Por fim, a classe de alto nível recebe apenas a abstração e não sabe qual canal está em uso:
public class PlaceOrderHandler
{
private readonly ICustomerRepository _repository;
private readonly INotificationService _notification;
public PlaceOrderHandler(ICustomerRepository repository, INotificationService notification)
{
_repository = repository;
_notification = notification;
}
public async Task HandleAsync(PlaceOrderCommand command)
{
var customer = await _repository.GetByIdAsync(command.CustomerId);
// ...
await _notification.SendAsync(customer, "Your order was placed successfully.");
}
}
Dessa forma, trocar e-mail por SMS, ou usar os dois, passa a ser uma decisão de configuração. Em outras palavras, a regra de negócio continua intacta, porque ela depende só do contrato que ela mesma definiu.
Benefícios do Dependency Inversion Principle
O primeiro benefício é a testabilidade, que já vimos nos exemplos anteriores. Como cada dependência é uma interface, o teste pode substituí-la por uma versão falsa e controlar exatamente o cenário.
O segundo é a manutenção. Quando um detalhe técnico muda, como a biblioteca de acesso a dados ou o provedor de e-mail, a alteração fica restrita à implementação e não se espalha pelo código de negócio.
O terceiro é a modularidade. Equipes diferentes podem trabalhar em paralelo, uma na regra de negócio e outra na infraestrutura, desde que ambas respeitem o mesmo contrato.
Por fim, o Dependency Inversion Principle deixa explícito o que cada classe precisa para funcionar. Basta olhar o construtor para saber quais colaboradores ela usa, o que torna o código mais fácil de entender.
Registrando as abstrações no container
Em produção, alguém precisa dizer qual implementação real deve ser entregue para cada interface. No ASP.NET Core, esse papel cabe ao container de injeção de dependência nativo.
Para isso, registramos o par interface e implementação na coleção de serviços da aplicação:
// Sempre que alguém pedir ICustomerRepository, entregue um CustomerRepository
services.AddScoped<ICustomerRepository, CustomerRepository>();
A partir daí, o framework cria o OrderController e injeta o repositório correto automaticamente. Nos testes, por outro lado, somos nós que entregamos a versão falsa.
Repare como os conceitos se encaixam. O Dependency Inversion Principle define que o controlador depende de ICustomerRepository, a DI entrega a dependência pelo construtor e o container de IoC decide qual implementação usar.
DIP e o princípio das dependências explícitas
O Dependency Inversion Principle costuma andar junto de outro princípio citado pela Microsoft, o Explicit Dependencies Principle. Ele diz que métodos e classes devem pedir explicitamente todos os colaboradores de que precisam para funcionar.
Na prática, o construtor é o lugar ideal para essa declaração. Quando uma classe recebe ICustomerRepository e INotificationService no construtor, ela deixa claro para quem a usa quais contratos precisam ser atendidos.
Por outro lado, uma classe que pode ser criada sem parâmetros, mas só funciona se alguma configuração global estiver pronta, está sendo desonesta com seus clientes. Afinal, o contrato do construtor promete uma coisa e o comportamento em tempo de execução exige outra.
Combinar os dois princípios gera um código autodocumentado. Assim, quem lê a classe entende suas dependências sem abrir a implementação, e quem escreve os testes sabe exatamente o que precisa simular.
Erros comuns ao aplicar o DIP
Primeiro, criar interfaces que vazam detalhes de implementação. Se ICustomerRepository recebesse uma SqlConnection como parâmetro, por exemplo, a abstração continuaria presa ao SQL Server.
Segundo, criar uma interface para absolutamente tudo, inclusive para classes simples que nunca terão outra implementação nem precisam de isolamento nos testes. Nesse caso, a abstração vira apenas burocracia.
Terceiro, injetar dependências demais no mesmo construtor. A Microsoft observa que um construtor com muitos parâmetros costuma indicar que a classe faz coisas demais e provavelmente viola o princípio da responsabilidade única.
Por fim, esconder dependências em vez de declará-las. Quando uma classe busca objetos em um container estático ou em variáveis globais, ela engana quem a consome, porque o construtor não mostra tudo de que a classe realmente precisa.
Conclusão
O Dependency Inversion Principle é um dos cinco princípios do SOLID e tem uma implementação relativamente fácil. Mesmo assim, ele nos ajuda muito a desacoplar as camadas do sistema.
Neste artigo, você viu as duas regras do princípio, entendeu por que ele fala em inversão e percebeu a diferença entre DIP, DI e IoC. Também partiu de um controlador preso a uma classe concreta e chegou a um código que aceita múltiplas implementações e testes sem banco de dados.
Na próxima vez que uma classe de negócio criar ou exigir um detalhe de infraestrutura, lembre do Dependency Inversion Principle e pergunte: qual abstração essa classe realmente precisa?
FAQ
O que diz o Dependency Inversion Principle?
Ele diz que módulos de alto nível e de baixo nível devem depender de abstrações. Além disso, as abstrações não devem depender de detalhes.
Qual a diferença entre DIP e Dependency Injection?
O DIP define que a classe deve depender de abstrações. A Dependency Injection é a técnica que entrega essas dependências à classe.
É possível usar injeção de dependência sem DIP?
Sim. Basta injetar uma classe concreta no construtor, mas assim você perde a flexibilidade de trocar a implementação.
Onde devo colocar as interfaces?
Junto do módulo de alto nível que as consome, normalmente na camada de domínio ou de aplicação. A infraestrutura apenas implementa esses contratos.
Preciso criar uma interface para toda classe?
Não. Crie abstrações quando houver mais de uma implementação possível ou quando precisar isolar a classe nos testes.