Inversão de controle e Injeção de dependência com .NET

 

Inversão de controle e injeção de dependência são conceitos fundamentais no desenvolvimento de software. Eles nos permitem criar sistemas mais flexíveis, testáveis e escaláveis, reduzindo o acoplamento entre os componentes e aumentando a coesão. Neste post, vamos explorar a importância desses conceitos através de um exemplo prático utilizando o .NET 8. 

De forma resumida, a inversão de controle é um princípio de design que diz que os módulos de alto nível não devem depender de módulos de baixo nível, mas sim de abstrações. Já a injeção de dependência é uma técnica que implementa esse princípio, permitindo que os componentes recebam suas dependências de fora, geralmente por meio de construtores ou propriedades, em vez de criá-las internamente. Dessa forma, a inversão de controle promove a desacoplação e a flexibilidade do sistema, enquanto a injeção de dependência é uma maneira de alcançar essa desacoplação na prática.

Para entendermos a importância, vamos começar com um exemplo bem simples que não utiliza a inversão de controle.

Criei uma Web Api do zero com o .NET 8 e excluí a controller e a classe WeatherForecast que vem de início como exemplo.

No lugar, criei uma classe que vai simular o nosso repositório, chamado ProdutoRepository:


É uma classe extremamente simples, com um método "RetornarProduto" que nos traz uma lista de strings, contendo "produto 1" e "produto 2". Num modelo real essa classe acessaria o banco e traria os produtos.

Em seguida, criamos a ProdutoController que vai chamar o repositório:


Até agora tudo certo, rodando a aplicação, conseguimos visualizar o Swagger, e esse endpoint devolve nossa lista de produtos conforme esperado.

Agora vamos adicionar em nosso projeto de teste, uma classe para testar a nossa controller.


Aqui nós criamos uma instância da ProdutoController, em seguida chamamos o método Get() que é o nosso endpoint. Testamos se o retorno não é nulo, pois deve retornar algo. Testamos se o tipo de retorno é um "OkObjectResult", e em seguida se o que contém no "Value" dele corresponde a uma lista de strings. Depois, se há 2 valores nessa lista, e se eles são "Produto 1" e "Produto 2".

Todos os testes passam.

Agora vamos analisar, no nosso exemplo sabemos exatamente quais são esses 2 produtos que retorna, mas obviamente no mundo real não temos como garantir quais retornos irão vir do banco. Poderíamos utilizar um banco de dados de teste para esse caso exclusivamente, mas a ideia não é realizar um teste de integração, então seria interessante termos a possibilidade e flexibilidade para mockar esse repositório.

Outro problema, voltando para nossa Controller. A ProdutoController tem controle direto sobre a criação e o uso do ProdutoRepository, o que viola o princípio da inversão de controle. Isso porque a ProdutoController está diretamente acoplada à implementação específica do ProdutoRepository, tornando-a menos flexível e difícil de testar.

Se alterarmos algo na classe de Repository, poderíamos impactar diretamente o funcionamento da controller, uma vez que está fortemente acoplada à sua implementação.

Agora, vamos fazer a inversão de controle.

Primeiro, vamos criar uma interface para o nosso ProdutoRepository:


Criamos a interface, e fazemos nossa classe implementar ela. Após isso, vamos até a Controller utilizar a interface com a injeção de dependência, ao invés de instanciar a classe de implementação.


Criamos um campo que armazena uma instância de classe que implementa a interface IProdutoRepositoy. Só poderá ser utilizado dentro da controller, por é um campo privado, e "readonly", somente leitura, então só pode ser atribuído durante a inicialização ou no construtor da classe e não pode ser alterado posteriormente.

Nesse momento, a controller não conhece a implementação de "RetornarProduto()" que ela está chamando.

Agora em nossa classe de teste, podemos mockar esse repositório. 


O que é um mock? é um objeto simulado que imita o comportamento de um real em um ambiente de teste. É util para isolar a área que está sendo testada, por exemplo, nós queremos apenas testar o comportamento da controller, de receber os dados vindos do repository e exibir para o usuário, queremos então testar se está realmente retornando um status code 200, e exibindo a lista de produtos. Aqui, por ser um teste de unidade, não estamos nos importando se o acesso do repositório ao banco está funcionando realmente (caso o repository fizesse isso), então, criamos primeiro uma instância do IProductRepository, chamada "mockProdutoRepository". No setup, vamos "configurar" o método que queremos, "RetornarProduto()". Ele não recebe nada como entrada. No Returns, nós dizemos o que deve ser retornado do repositório. Note que passamos "Produto 1 Teste", "Produto 2 Teste", diferente do que retorna realmente em nossa implementação, ou seja, não nos importamos com a implementação real da classe, nossa intenção não é testar ela.

Agora, instanciamos a ProdutoController, e passamos nosso mock "mockProdutoRepository.Object", ou seja, nós injetamos essa informação "simulada" em nossa controller. Agora ela vai ser instanciada, seguir toda a lógica que poderia ter, mas a chamada ao repositório vai retornar o que definimos, e vamos conseguir testar se o retorno vai ser realmente uma lista de 2 produtos, com aqueles nomes, e um StatusCode 200.

Da forma antiga não conseguimos fazer isso, pois a controller estava instanciando uma classe ProdutoRepository dentro do método testado. 

Agora com os testes funcionando (tudo rodou certinho, pode confiar! hahah), vamos rodar o projeto e testar nosso método Get. 

Opa, pera lá, estourou um erro:

System.InvalidOperationException: Unable to resolve service for type '<interface>' while attempting to activate

O que isso significa? Nós não registramos nosso serviço no container de injeção de dependência. Então vamos fazê-lo. Como estou no .NET 8, isso é feito na classe Program. No .NET 5 seria na Startup.


Serviço registrado, agora vamos testar:


E voilà! Tudo funcionando.

Assim como o teste "injetou" o mock do Repository, nós injetamos a implementação de "ProdutoRepository" nessa controller, já que não tem ninguém de fato instanciando a Controller quando a aplicação roda, então o próprio .NET faz isso para nós. 

A vantagem é que se amanhã decidirmos mudar a forma de buscar os dados, por exemplo, não é mais trazendo aqueles 2 produtos fixos, nós vamos usar o banco de dados real, vamos modificar a implementação e a controller não vai ter impacto nenhum, pois ela não se importa de onde vem o dado, apenas com o contrato que está seguindo, que é o da interface possuindo aquele método, que não recebe parâmetros e devolve uma lista de strings. Poderíamos sentir impacto se por exemplo, na implementação passarmos a receber algo no construtor padrão da classe, que exigiria que a controller passasse também esses componentes na hora de instanciar.

Podemos ir além em nossos testes, veja bem:


Criamos uma outra classe que implementa a mesma interface. Agora, a lista retorna dados diferentes.

Basta registrar no container (classe Program) que agora a interface é implementada por ProdutoProducaoRepository, e aí estão nossos produtos de "produção":


Bacana demais né?

Pois é isso. Acredito que esses conceitos são importantes pois ao longo do nosso aprendizado, costumamos apenas seguir como deve ser feito, criar interfaces, implementações, utilizá-las no construtor, se der erro registrar nas configurações, mas as vezes sem entender o porquê dessas coisas, qual a sua importância. Muitas vezes parece um monte de "complicações" desnecessárias, então é sempre bom ver como seria muito mais difícil sem essas práticas.

Postar um comentário

0 Comentários