Conceito
Imagine que você tem uma aplicação pequena, um sistema de padaria em que os usuários fazem a gestão do seu negócio. Então, a medida que seu aplicativo começa a ganhar popularidade, surge a a demanda de implementar novas funcionalidades, e consequentemente migrar para um banco de dados mais robusto e escalável. No entanto, seu sistema possui uma comunicação acoplada e simplificada com a camada de dados, esse fato torna a tarefa árdua, já que esse acoplamento entre a camada de dados (Domain Model) e aplicação obriga aos desenvolvedores a analisar todos os lugares, já que cada ponto que interage com o banco de dados precisa ser alterado manualmente, tornando uma atividade tediosa e propensa a erros.
O Padrão Repositório surge para evitar que situações como essas não aconteçam. O objetivo é desacoplar o nosso código da camada de dados (Domain Model) para que problemas como esse não aconteça. Então, podemos dizer que a aplicação não sabe qual o banco de dados está sendo usado, apenas o repositório que comunica para isso para a nossa aplicação.
Além disso, há outras vantagens, como:
- Evitar códigos duplicados
- Injeção de dependência
- Facilita testes unitários
- Flexibilidade (você pode facilmente trocar mecanismo de armazenamento (por exemplo um banco de dados SQL para um NoSQL) sem afetar o código dos negócios
Entendendo todos os conceitos acima, podemos afirmar dois fatos antes de continuar:
Não implementando o padrão repositório
A aplicação interage diretamente com o banco de dados.
(Banco de dados → Aplicação)
Implementando o padrão repositório
A aplicação usa o repositório como intermediador da comunicação entre aplicação e banco de dados.
(Banco de dados → Repositório → Aplicação)
Antes de implementar o Padrão Repository
Antes de começar a implementar a interface, vamos mostrar a versão do sem esse padrão de projeto?
[ApiController]
[Route("api/[controller]")]
public class ProdutosController : ControllerBase
{
private readonly MeuDbContext _contexto;
public ProdutosController(MeuDbContext contexto)
{
_contexto = contexto;
}
[HttpGet]
public IActionResult ObterTodosProdutos()
{
var produtos = _contexto.Produtos.ToList();
return Ok(produtos);
}
[HttpGet("{id}")]
public IActionResult ObterProdutoPorId(int id)
{
var produto = _contexto.Produtos.FirstOrDefault(p => p.Id == id);
if (produto == null)
{
return NotFound();
}
return Ok(produto);
}
[HttpPost]
public IActionResult CriarProduto(Produto produto)
{
_contexto.Produtos.Add(produto);
_contexto.SaveChanges();
return CreatedAtAction(nameof(ObterProdutoPorId), new { id = produto.Id }, produto);
}
}
Controller sem o uso do padrão Repository
Podemos ver nesse exemplo que temos alguns pontos a melhorar, como:
Acoplamento do Controller ao EF Core
Nosso Controller
está a todo momento instanciando o contexto do banco de dados e usando diretamente na nossa camada de aplicação. Criando um acoplamento e dependência direta entre nossa camada de dados e aplicação
Concentração e responsabilidade de acesso a dados na mão do Controller
Em consequência do ponto anterior, além do acoplamento entre o Controller
e camada de dados, a responsabilidade dos nossos métodos de acesso aos dados está concentrado no controller, deixando o código mais difícil de testar, pois mistura a lógica de negócios com lógica de acesso a dados.
Falta de reutilização de código
Quando não usamos o padrão Repository não existe uma abstração clara sobre como os dados são acessados e manipulados, então teríamos que repetir então os trechos de códigos de acesso aos dados como: _contexto[...]
em todos os Controllers, o que pode levar a inconsistências e erros, pois o código pode ser modificado em um lugar e ser esquecido em outro.
Sabendo disso, ao introduzir o padrão Repository, criamos uma camada de abstração entre a lógica de negócios da aplicação e o mecanismo de armazenamento de dados. Isso permite que os controllers e outras partes da aplicação interajam com os dados por meio de uma interface bem definida, em vez de dependerem diretamente de detalhes de implementação, como o ORM utilizado ou a estrutura do banco de dados.
Implementando o Padrão Repository
Para a implementação do padrão é necessário seguir algumas etapas para garantir a estruturação correta e a eficiência da separação das responsabilidades.
- Interface do Repositório
- Classe concreta que implementa a interface criada
- Controller para usar o repositório
Vamos usar o exemplo do código anterior sem a implementação e adequá-lo.
Interface
A interface é a primeira etapa, ela é a responsável por definir o contrato (conjunto de operações) que os repositórios devem implementar e que será implementada pela classe concreta (próxima etapa). Simplificando, esse contrato são operações básicas de acesso aos dados, como buscar, adicionar, atualizar e excluir registros, definindo uma fronteira clara entre as partes da aplicação sem que haja uma dependência entre elas.
Para construir a interface temos que identificar a Entidade do Domínio e Métodos necessários para acessar e manipular os dados da Entidade.
-
[1] - Prática
Então, ao analisar o nosso Controller, podemos ver que o nome da entidade é
Produto
e seus métodos sãoObterTodosProdutos
,ObterProdutoPorId
eCriarProduto
[ApiController] [Route("api/[controller]")] public class **ProdutosController** : ControllerBase { private readonly MeuDbContext _contexto; public ProdutosController(MeuDbContext contexto) { _contexto = contexto; } [HttpGet] public IActionResult **ObterTodosProdutos**() { var produtos = _contexto.Produtos.ToList(); return Ok(produtos); } [HttpGet("{id}")] public IActionResult **ObterProdutoPorId**(int id) { var produto = _contexto.Produtos.FirstOrDefault(p => p.Id == id); if (produto == null) { return NotFound(); } return Ok(produto); } [HttpPost] public IActionResult **CriarProduto**(Produto produto) { _contexto.Produtos.Add(produto); _contexto.SaveChanges(); return CreatedAtAction(nameof(ObterProdutoPorId), new { id = produto.Id }, produto); } }
O processo da criação da interface é simples, apenas será criado os contratos com o nome da interface (seguindo as convenções) e seus métodos.
public interface **IProdutoRepository** { IEnumerable<Produto> **ObterTodosProdutos**(); Produto **ObterProdutoPorId**(int id); void **CriarProduto**(Produto produto); }
Implementando o Generic Repository
Para entender a implementação do repositório genérico, é importante entender conceitos acerca de Interfaces genéricas.
Vamos imaginar que seu projeto agora possui várias entidades, e estas entidades terão várias modelagens de DataAcess para cada classe.
Tendo esse contexto em mente, teremos que criar várias interfaces e repositórios e em seguida implementar esses métodos para cada entidade distinta. É nesse momento que entra o uso do Repositório Genérico, já que ele permite que você abstraia o acesso aos dados em uma única classe. Essa prática permite economizar mais tempo e deixar o código menos acoplado, além de seguir o princípio DRY.
Para entender com melhor eficiência sobre os repositórios genéricos pode-se dizer que precisamos dividir duas etapas em nosso exemplo, a primeira que é a criação do repositório genérico em si, enquanto a segunda será a implementação desse repositório.
- Criando a interface genérica
- Criando interface genérica
- Implementando interface genérica
- Implementação de classe específica
1a. Criando a Interface Genérica
Vamos começar a implementação da interface genérica IBaseRepository
.
public interface IBaseRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(int id);
Task<T> AddAsync(T entity);
Task<T> UpdateAsync(T entity);
DeleteAsync(int id);
}
1b. Implementando Interface Genérica
Agora vamos fazer a implementação da interface IBaseRepository
. A partir dessa implementação, nossas entidades podem utilizar os métodos genéricos contidos nessa implementação.
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
public class BaseRepository<T> : IBaseRepository<T> where T : class
{
protected readonly DbContext _context;
public BaseRepository(DbContext context)
{
_context = context;
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _context.Set<T>().ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _context.Set<T>().FindAsync(id);
}
public async Task AddAsync(T entity)
{
await _context.Set<T>().AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_context.Set<T>().Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await _context.Set<T>().FindAsync(id);
if (entity != null)
{
_context.Set<T>().Remove(entity);
await _context.SaveChangesAsync();
}
}
}
2a. Entidade da Classe Específica
Agora que temos nossa classe genérica base vamos implementar uma Entidade em nosso sistema chamada Cliente
.
public class Cliente
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
2b. Interface da Classe Específica
Observe que não repetimos oos método que estão no IBaseRepository
, uma vez que são herdados, apenas criamos um método específico que não faz parte da classe base
public interface IClienteRepository : IBaseRepository<Cliente>
{
Task<IEnumerable<Cliente>> GetCostumerWithOrderAsync();
}
3b. Implementação da Classe Específica
public class ClienteRepository : BaseRepository<Cliente>, IClienteRepository
{
public RestauranteRepository(DbContext context) : base(context)
{
}
public async Task<IEnumerable<Cliente>> GetCustomersWithOrdersAsync()
{
throw new NotImplementedException();
}
}
Podemos entender a partir desse momento que se tivermos uma nova entidade chamada Atendente
teremos o repositório genérico que fornece todos os métodos básicos para a manutenção de nossos acessos aos dados. E além disso, cada interface dedicada pode implementar uma lógica extra de negócio/dados sem interferir na implementação de outras entidades. Além disso tudo, temos um código mais coeso e com menos duplicação de código, respeitando o princípio DRY.
Top comments (0)