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 a 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). Então, 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);
}
}
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
Vamos reestruturar o exemplo anterior, que ainda não utiliza o padrão Repository, para aplicar esse padrão de forma clara e eficiente.
A implementação do padrão Repository segue algumas etapas fundamentais que garantem a separação adequada de responsabilidades e facilitam a manutenção do código:
- Definir a interface do repositório, contendo os métodos que serão expostos à camada de aplicação.
- Criar a classe concreta, que implementa a interface e realiza o acesso real aos dados (por exemplo, via EF Core ou Dapper).
- Utilizar o repositório no Controller, garantindo que a camada de apresentação interaja apenas com a abstração do repositório, e não com os detalhes da persistência.
1. Implementando 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.
Então, ao analisar o nosso Controller, podemos ver que o nome da entidade é Produto
e seus métodos são ObterTodosProdutos
, ObterProdutoPorId
e CriarProduto
[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);
}
2. Definindo a classe classe concreta
Com a nossa interface construída, em seguida precisamos criar nossa classe concretas que vão implementar as interfaces dos repositórios. A classe concreta é a responsável por fornecer a lógica real de consultas e acesso aos dados de uma determinada entidade.
Em nosso exemplo estamos usando a ORM Entity Framework Core. Em nossa implementação assinamos todos os métodos que foram criados no contratos e todos eles fazem uma interação específica com _contexto
do banco de dados, separando a responsabilidade de acesso aos dados que anteriormente era do controller
para essa nova classe Repository.
public class ProdutoRepository : IProdutoRepository
{
private readonly MeuDbContext _contexto;
public ProdutoRepository(MeuDbContext contexto)
{
_contexto = contexto;
}
public IEnumerable<Produto> ObterTodosProdutos()
{
return _contexto.Produtos.ToList();
}
public Produto ObterProdutoPorId(int id)
{
return _contexto.Produtos.FirstOrDefault(p => p.Id == id);
}
public void CriarProduto(Produto produto)
{
_contexto.Produtos.Add(produto);
_contexto.SaveChanges();
}
}
Não se esqueça de configurar a injeção de dependência na classe principal.
Configurar a Injeção de Dependência
Certifique-se de que a injeção de dependência para está configurada corretamente no seu projeto. Existem duas formas de fazer isso, e a escolha depende do tamanho do seu projeto e escalabilidade.
Cadastro padrão
var builder = WebApplication.CreateBuilder(args);
// Registro do repositório e manager no container de serviços
builder.Services.AddScoped<IContaRepository, ContaRepository>();
var app = builder.Build();
Cadastro com arquivo de configuração
1. Criar um arquivo de configuração
using AL.Data.Repository;
using AL.Manager.Implementation;
using AL.Manager.Interfaces.Managers;
using AL.Manager.Interfaces.Repositories;
namespace CL.WebApi.Configuration;
public static class DependencyInjectionConfig
{
public static void AddDependencyInjectionConfiguration(this IServiceCollection services)
{
services.AddScoped<IContaRepository, ContaRepository>();
services.AddScoped<IContaManager, ContaManager>();
}
}
2. Cadastrar arquivo no Startup
using AL.WebApi.Configuration;
using CL.WebApi.Configuration;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDataBaseConfiguration(builder.Configuration);
**builder.Services.AddDependencyInjectionConfiguration();**
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseDatabaseConfiguration();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
3. Utilizar repositório no Controller
A responsabilidade do nosso controller agora será apenas receber requisições HTTP relacionadas a nossa Entidade (Produto) e coordenar as operações efetuar as requisições. Será então, apenas um intermediador entre o cliente e o repositório de produtos. Com isso, temos a melhor separação de responsabilidades e testes, sem contar o menor acoplamento entre a camada de dados e negócios.
Certo, mas como isso acontece? Se formos comparar o código inicial, podemos ver que tínhamos em nosso construtor a instância do contexto banco de dados e esse contexto era o responsável por lidar com as operações do banco de dados além das requisições HTTP, levando a um forte acoplamento. Mas agora efetuamos uma injeção de dependência em nosso repositório de produtos no construtor do controller, introduzindo uma abstração entre nosso controller e o acesso aos dados. Então, o controller não precisa se preocupar com os detalhes de como os dados estão sendo acessados, pois ao invés disso ele apenas se preocupa em chamar os métodos do repositório (que criamos anteriormente) que faz esse trabalho, resultando em um código mais limpo, modular, escalável e com separações de responsabilidade.
A seguir o exemplo do código com versão inicial e versão com padrão repositório.
[ApiController]
[Route("api/[controller]")]
public class ProdutosController : ControllerBase
{
private readonly IProdutoRepository _produtoRepository;
public ProdutosController(IProdutoRepository produtoRepository)
{
_produtoRepository = produtoRepository;
}
[HttpGet]
public IActionResult ObterTodosProdutos()
{
var produtos = _produtoRepository.ObterTodosProdutos();
return Ok(produtos);
}
[HttpGet("{id}")]
public IActionResult ObterProdutoPorId(int id)
{
var produto = _produtoRepository.ObterProdutoPorId(id);
if (produto == null)
{
return NotFound();
}
return Ok(produto);
}
[HttpPost]
public IActionResult CriarProduto(Produto produto)
{
_produtoRepository.CriarProduto(produto);
return CreatedAtAction(nameof(ObterProdutoPorId), new { id = produto.Id }, produto);
}
}
Top comments (0)