Interfaces
O Papel das Interfaces no Repository Pattern
Introdução e Conceitos Fundamentais
Neste capítulo, abordaremos o papel crucial das interfaces na implementação do Repository Pattern e sua importância fundamental na organização, manutenção e testabilidade do código. O uso estratégico de interfaces estabelece uma separação clara de responsabilidades entre as camadas da aplicação, promovendo um design mais flexível e sustentável.
As interfaces funcionam como contratos explícitos para acesso a dados, descrevendo o que pode ser feito sem expor como essas operações são realizadas. Essa abstração permite que a camada de domínio ou aplicação dependa apenas de definições estáveis, isolando-se de detalhes de implementação específicos de infraestrutura, como o Entity Framework Core, ADO.NET, ou qualquer outra tecnologia de persistência.
Benefícios do Uso de Interfaces
Baixo Acoplamento: Desacopla completamente a lógica de negócio dos detalhes de infraestrutura de dados.
Substituibilidade de Implementações: Facilita a troca de tecnologias de persistência (ex: Entity Framework para Dapper) ou fontes de dados (ex: SQL Server para PostgreSQL) sem impactar o domínio.
Testabilidade Aprimorada: Permite a criação de simulações (mocks, fakes, stubs) para testes automatizados isolados.
Evolutividade do Sistema: Alterações internas no repositório não causam impacto nas camadas consumidoras.
Clareza Arquitetural: Estabelece um limite explícito entre a lógica de negócio e a infraestrutura de dados.
No contexto do Repository Pattern, as interfaces representam o ponto de entrada oficial para todas as operações de persistência — criação, leitura, atualização e remoção de entidades (CRUD). Esta abordagem formaliza a comunicação entre camadas e estabelece uma fronteira bem definida que respeita os princípios da Arquitetura Limpa e Domain-Driven Design.
Do Problema à Solução: Uma Jornada Prática
O Cenário Inicial: Acoplamento Direto
Vamos começar com um cenário comum onde um endpoint API depende diretamente do DbContext:
Esta abordagem apresenta vários problemas:
Difícil testabilidade (requer banco de dados real)
Lógica de acesso a dados misturada com a camada de apresentação
Dificuldade em substituir a implementação de persistência
Primeiro Passo: Introduzindo o Repository Concreto
Uma melhoria inicial é encapsular o acesso a dados em uma classe dedicada:
No Program.cs, registramos e utilizamos este repositório:
Progresso alcançado: O código agora está mais organizado, mas ainda enfrentamos o desafio da testabilidade, pois os testes precisariam de um banco de dados real ou de um DbContext simulado complexo.
A Barreira da Testabilidade e a Necessidade de Abstração
Para testar unidades de forma isolada, não podemos depender de infraestrutura real como bancos de dados. Precisamos de uma maneira de simular o comportamento do repositório. Uma tentativa inicial poderia ser:
Contudo, mesmo com um FakeProductRepository correto, enfrentaríamos outro problema prático: precisaríamos alterar manualmente o Program.cs para usar a implementação fake durante os testes:
Esta abordagem é inviável e propensa a erros. A solução reside no uso de interfaces e inversão de dependência.
Implementando a Abstração com Interfaces
Definindo o Contrato: IProductRepository
Criamos um diretório Repositories/Abstractions para organizar nossas interfaces e definimos um contrato claro:
Nota sobre design:
A assinatura do método DeleteAsync pode-se ser ajustada para retornar bool (indicando sucesso/falha) e receber id em vez do objeto completo, seguindo boas práticas comuns.
Implementação para Produção
Implementação para Testes: FakeProductRepository
Diferenciando Fake, Mock e Stub
É importante compreender a distinção entre esses tipos de objetos de teste:
Fake: Implementação funcional simplificada (como nosso
FakeProductRepository). Possui lógica real, mas simplificada, sem dependências externas.Mock: Objeto que simula comportamento e registra interações para verificação posterior. Usado com frameworks como Moq ou NSubstitute.
Stub: Fornece respostas pré-definidas para chamadas específicas, sem lógica significativa.
Nosso FakeProductRepository é, na verdade, um Fake genuíno, pois implementa uma versão funcional (em memória) do contrato sem depender de infraestrutura externa.
Configuração de Injeção de Dependência
Princípio da Inversão de Dependência (DIP)
Agora aplicamos o Princípio da Inversão de Dependência (DIP), que estabelece que:
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
Abstrações não devem depender de detalhes. Detalhes devem depender de abstrações.
No Program.cs, configuramos o sistema de injeção de dependência para respeitar este princípio:
Escopos de Vida de Serviços
Ao registrar serviços, podemos escolher entre três escopos de vida diferentes:
Transient
Nova instância a cada solicitação
Serviços stateless, leves, sem custo de criação
Quando não há estado ou o estado não precisa ser compartilhado
Scoped
Uma instância por requisição HTTP
DbContext, repositórios, serviços com estado por requisição
Para a maioria dos cenários em aplicações web
Singleton
Uma instância para toda a aplicação
Caches, clientes HTTP configurados, serviços de configuração
Quando o estado precisa ser compartilhado globalmente
Compreendendo os Escopos de Vida em Profundidade
A escolha do escopo de vida afeta significativamente o comportamento da aplicação:
Transient: Ideal para serviços que são stateless ou têm um custo de criação muito baixo. Cada vez que o serviço é solicitado (mesmo dentro da mesma requisição), uma nova instância é criada. Use com cautela para serviços pesados, pois pode impactar o desempenho.
Scoped: O escopo mais comum para aplicações web. A instância é criada no início de uma requisição HTTP e compartilhada por todos os componentes que a solicitam durante aquela requisição. É seguro para a maioria dos cenários, incluindo
DbContextno Entity Framework Core, que gerencia conexões de banco de dados e rastreamento de entidades.Singleton: Use apenas para serviços verdadeiramente globais e thread-safe. A instância é criada na primeira solicitação e reutilizada por toda a vida da aplicação. Deve-se evitar usar Singleton para serviços que dependem de serviços Scoped ou Transient, a menos que sejam gerenciados cuidadosamente.
Para nosso repositório, AddTransient é uma escolha comum, mas AddScoped pode ser mais apropriada se o repositório mantiver estado durante uma requisição.
Uso nos Endpoints
Com a interface definida e o serviço registrado, podemos refatorar nossos endpoints:
Agora, para alternar entre ambientes de teste e produção, basta alterar uma única linha no Program.cs:
Benefícios da Abordagem Completa
Testabilidade Prática
Com a interface em vigor, podemos escrever testes unitários eficazes:
Flexibilidade de Implementação
A interface permite múltiplas implementações para diferentes cenários:
Conclusões e Boas Práticas
Resumo da Jornada
Começamos com acoplamento direto ao
DbContext, o que dificultava testes e manutenção.Introduzimos um repositório concreto, melhorando a organização mas mantendo problemas de testabilidade.
Identificamos a necessidade de simulação para testes isolados.
Implementamos uma interface (
IProductRepository) como contrato formal.Criamos múltiplas implementações: uma para produção (
ProductRepository) e uma para testes (FakeProductRepository).Configuramos a injeção de dependência usando o Princípio da Inversão de Dependência.
Alcançamos um design flexível, testável e em conformidade com os princípios SOLID.
Boas Práticas Recomendadas
Sempre defina interfaces para seus repositórios, mesmo em projetos pequenos.
Organize suas interfaces em um diretório
AbstractionsouContractspara clareza arquitetural.Use nomes descritivos para métodos de repositório, refletindo a intenção do domínio.
Considere a paginação desde o início em métodos que retornam coleções.
Documente as exceções que cada método pode lançar na interface.
Para projetos complexos, considere um repositório genérico base (
IRepository<T>).Escolha o escopo de vida apropriado (
Transient,Scoped,Singleton) baseado no uso do repositório.
Aplicação em Outros Contextos
Este padrão não se limita a produtos ou a Entity Framework. A mesma abordagem pode ser aplicada para:
Repositórios para qualquer entidade (
IUserRepository,IOrderRepository, etc.)Diferentes tecnologias de persistência (Dapper, MongoDB, APIs externas)
Serviços de domínio que precisam de simulação para testes
A criação de uma interface de repositório estabelece um contrato claro que serve como fundamento para um código mais limpo, mais testável e mais sustentável, preparando a base para uma arquitetura que pode evoluir com as necessidades do negócio sem reescritas significativas.
Atualizado