Repositório Genérico - Violando o princípio ISP


   Neste artigo veremos como o uso do Repositório Genérico pode violar o princípio SOLID ISP - Interface Segregation Principle.

Um repositório genérico é uma classe ou interface que define um conjunto de operações padrão para acesso a dados. Essas operações incluem adicionar, atualizar, excluir e consultar dados em um banco de dados ou outra fonte de dados.

O repositório genérico é uma abstração que pode ser usada para lidar com diferentes tipos de entidades de dados em um sistema. Ele geralmente é implementado como uma classe genérica, que pode receber um tipo de entidade específico como parâmetro de tipo genérico.

As vantagens do uso de um repositório genérico incluem:

As desvantagens do uso de um repositório genérico incluem:

Além das desvatangens apresentadas vamos incluir aqui que o uso de um repositório genérico geralmente nos leva a violar o princípio SOLID ISP e é nisto que vou focar neste artigo.

Violando o principio ISP

Vamos definir um cenário bem simples onde devemos tratar com Pedidos e Itens de Pedidos em uma aplicação web e onde estamos usando o EF Core que usamos para definir um contexto e realizar o mapeamento ORM.

Para isso temos uma interface genérica de repositório com métodos CRUD básicos, como o exemplo abaixo:

public interface IRepository<T> where T : class
{
    void Add(T entity);
    void Delete(T entity);
    void Update(T entity);
    T GetById(int id);
    IEnumerable<T> GetAll();
}

Com essa interface, é possível criar um repositório genérico para qualquer entidade, incluindo as entidades Pedido e ItemPedido:

public class Repository<T> : IRepository<T> where T : class
{
    private readonly DbContext _context;
    public Repository(DbContext context)
    {
        _context = context;
    }
    public void Add(T entity)
    {
        _context.Set<T>().Add(entity);
        _context.SaveChanges();
    }
    public void Delete(T entity)
    {
        _context.Set<T>().Remove(entity);
        _context.SaveChanges();
    }
    public void Update(T entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
        _context.SaveChanges();
    }
    public T GetById(int id)
    {
        return _context.Set<T>().Find(id);
    }
    public IEnumerable<T> GetAll()
    {
        return _context.Set<T>().ToList();
    }
}

Embora o uso de um repositório genérico possa parecer vantajoso por ser mais fácil de implementar e economizar tempo, ele pode violar o princípio ISP se houver necessidade de adicionar funcionalidades específicas para entidades específicas.

Para ilustrar isso vamos considerar que na implementação do repositório de pedidos precisamos obter os pedidos por data e para isso fizemos a seguinte implementação :

1- interface IPedidoRepository

public interface IPedidoRepository : IRepository<Pedido>
{
    IEnumerable<Pedido> GetPedidosPorData(DateTime data);
}

2- classe PedidoRepository

public class PedidoRepository : Repository<Pedido>, IPedidoRepository
{
  
public PedidoRepository(DbContext context) : base(context)
   {
     
public IEnumerable<Pedido> GetPedidosPorData(DateTime data)
      {
         
return _context.Set<Pedido>().Where(p => p.Data == data).ToList();
      }
   }
}

Nesse caso, a interface IPedidoRepository herda a interface genérica IRepository<T> e adiciona um novo método GetPedidosPorData. O problema é que a classe Repository<T> não implementa esse método, e, portanto, a classe PedidoRepository precisa implementá-lo diretamente.

Isso viola o princípio ISP, pois obriga a classe PedidoRepository a implementar métodos que não são necessários para outras entidades e, portanto, pode resultar em uma classe com muitas responsabilidades e difícil de manter.

Para evitar essa violação do ISP, seria necessário criar uma interface separada para cada entidade, permitindo que cada uma tenha seus próprios métodos específicos e evitando que as implementações sejam forçadas a ter métodos desnecessários.

Para isso podemos definir uma interface IPedidoRepository:

public interface IPedidoRepository
{
  
void Add(Pedido pedido);
  
void Delete(Pedido pedido);
  
void Update(Pedido pedido);
   Pedido GetById(
int id);
   IEnumerable<Pedido> GetAll();
   IEnumerable<Pedido> GetPedidosPorData(DateTime data);
}

E então criar uma classe concreta de repositório PedidoRepository que implementa essa interface e fornece a lógica específica para a entidade Pedido:

public class PedidoRepository : IPedidoRepository
{
    private readonly DbContext _context;

    public PedidoRepository(DbContext context)
    {
        _context = context;
    }

    public void Add(Pedido pedido)
    {
        _context.Set<Pedido>().Add(pedido);
        _context.SaveChanges();
    }

    public void Delete(Pedido pedido)
    {
        _context.Set<Pedido>().Remove(pedido);
        _context.SaveChanges();
    }

    public void Update(Pedido pedido)
    {
        _context.Entry(pedido).State = EntityState.Modified;
        _context.SaveChanges();
    }

    public Pedido GetById(int id)
    {
        return _context.Set<Pedido>().Find(id);
    }

    public IEnumerable<Pedido> GetAll()
    {
        return _context.Set<Pedido>().ToList();
    }

    public IEnumerable<Pedido> GetPedidosPorData(DateTime data)
    {
        return _context.Set<Pedido>().Where(p => p.Data == data).ToList();
    }
}

Observe que a classe PedidoRepository não herda de uma classe genérica de repositório, mas sim implementa a interface IPedidoRepository e fornece a lógica específica para a entidade Pedido.

Dessa forma, podemos criar classes de repositório separadas para cada entidade e fornecer a lógica específica necessária para cada uma, evitando a violação do princípio ISP. E, para usar esses repositórios, podemos injetar as dependências correspondentes em outras classes de negócio ou controladores de API.

Como evitar a violação do ISP em repositórios genéricos ?

Interfaces Coesas: Crie interfaces específicas para cada tipo de cliente, expondo apenas os métodos necessários para cada caso

public interface IReadOnlyRepository<T> where T : class
{
    T GetById(int id);
}
public interface IWriteOnlyRepository<T> where T : class
{
    void Create(T entity);
    void Update(T entity);
    void Delete(T entity);
}

Interfaces Base: Utilize uma interface base para definir métodos comuns e interfaces mais específicas para funcionalidades adicionais.

public interface IRepository<T> where T : class
{
    T GetById(int id);
}
public interface IWriteRepository<T> : IRepository<T> where T : class
{
    void Create(T entity);
    void Update(T entity);
    void Delete(T entity);
}

Ao aplicar o ISP em seus repositórios genéricos, você estará construindo um sistema mais flexível, escalável e de fácil manutenção. Ao evitar interfaces "gordas" e garantir que cada cliente dependa apenas do necessário, você estará promovendo um design mais coeso e modular.

Naturamente devemos levar em conta a relação custo/benefício que a implementação ou não do repositório genérico poderá nos trazer. Se valer a pena , que se dane a violação do princípio ISP, desde que isso não traga efeitos colaterais indesejáveis que vão afetar a nossa aplicação.

E estamos conversados...

"Ensina-me, Senhor, o teu caminho, e andarei na tua verdade; une o meu coração ao temor do teu nome."
Salmos 86:11

Referências:


José Carlos Macoratti