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

  1. Baixo Acoplamento: Desacopla completamente a lógica de negócio dos detalhes de infraestrutura de dados.

  2. 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.

  3. Testabilidade Aprimorada: Permite a criação de simulações (mocks, fakes, stubs) para testes automatizados isolados.

  4. Evolutividade do Sistema: Alterações internas no repositório não causam impacto nas camadas consumidoras.

  5. 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:

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

  2. 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:

Escopo
Tempo de Vida
Uso Típico
Quando Usar

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 DbContext no 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

  1. Começamos com acoplamento direto ao DbContext, o que dificultava testes e manutenção.

  2. Introduzimos um repositório concreto, melhorando a organização mas mantendo problemas de testabilidade.

  3. Identificamos a necessidade de simulação para testes isolados.

  4. Implementamos uma interface (IProductRepository) como contrato formal.

  5. Criamos múltiplas implementações: uma para produção (ProductRepository) e uma para testes (FakeProductRepository).

  6. Configuramos a injeção de dependência usando o Princípio da Inversão de Dependência.

  7. Alcançamos um design flexível, testável e em conformidade com os princípios SOLID.

Boas Práticas Recomendadas

  1. Sempre defina interfaces para seus repositórios, mesmo em projetos pequenos.

  2. Organize suas interfaces em um diretório Abstractions ou Contracts para clareza arquitetural.

  3. Use nomes descritivos para métodos de repositório, refletindo a intenção do domínio.

  4. Considere a paginação desde o início em métodos que retornam coleções.

  5. Documente as exceções que cada método pode lançar na interface.

  6. Para projetos complexos, considere um repositório genérico base (IRepository<T>).

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