Vantagem de criar um repositório genérico vs. repositório específico para cada objeto?

132

Estamos desenvolvendo um aplicativo ASP.NET MVC e agora estamos construindo as classes de repositório / serviço. Eu estou querendo saber se há alguma grande vantagem em criar uma interface genérica de IRepository que todos os repositórios implementam, contra cada Repositório com sua própria interface e conjunto de métodos.

Por exemplo: uma interface genérica de IRepository pode parecer (tirada desta resposta ):

public interface IRepository : IDisposable
{
    T[] GetAll<T>();
    T[] GetAll<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter);
    T GetSingle<T>(Expression<Func<T, bool>> filter, List<Expression<Func<T, object>>> subSelectors);
    void Delete<T>(T entity);
    void Add<T>(T entity);
    int SaveChanges();
    DbTransaction BeginTransaction();
}

Cada repositório implementaria essa interface, por exemplo:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • etc.

A alternativa que seguimos em projetos anteriores seria:

public interface IInvoiceRepository : IDisposable
{
    EntityCollection<InvoiceEntity> GetAllInvoices(int accountId);
    EntityCollection<InvoiceEntity> GetAllInvoices(DateTime theDate);
    InvoiceEntity GetSingleInvoice(int id, bool doFetchRelated);
    InvoiceEntity GetSingleInvoice(DateTime invoiceDate, int accountId); //unique
    InvoiceEntity CreateInvoice();
    InvoiceLineEntity CreateInvoiceLine();
    void SaveChanges(InvoiceEntity); //handles inserts or updates
    void DeleteInvoice(InvoiceEntity);
    void DeleteInvoiceLine(InvoiceLineEntity);
}

No segundo caso, as expressões (LINQ ou não) estariam totalmente contidas na implementação do Repositório, quem estiver implementando o serviço só precisa saber para qual função do repositório chamar.

Acho que não vejo a vantagem de escrever toda a sintaxe da expressão na classe de serviço e passar para o repositório. Isso não significa que o código LINQ de fácil envio de mensagens está sendo duplicado em muitos casos?

Por exemplo, em nosso antigo sistema de faturamento, chamamos

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

de alguns serviços diferentes (cliente, fatura, conta etc.). Isso parece muito mais limpo do que escrever o seguinte em vários lugares:

rep.GetSingle(x => x.AccountId = someId && x.InvoiceDate = someDate.Date);

A única desvantagem que vejo ao usar a abordagem específica é que poderíamos terminar com muitas permutações das funções Get *, mas isso ainda parece preferível ao empurrar a lógica da expressão para as classes Service.

o que estou perdendo?

Bip Bip
fonte
Usar repositórios genéricos com ORMs completos parece inútil. Eu discuti isso em detalhes aqui .
Amit Joshi

Respostas:

169

Esse é um problema tão antigo quanto o próprio padrão do repositório. A recente introdução de LINQs IQueryable, uma representação uniforme de uma consulta, causou muita discussão sobre esse mesmo tópico.

Eu prefiro repositórios específicos, depois de ter trabalhado muito duro para construir uma estrutura de repositório genérica. Não importa qual mecanismo inteligente eu tentei, sempre acabava com o mesmo problema: um repositório é uma parte do domínio que está sendo modelado e esse domínio não é genérico. Nem toda entidade pode ser excluída, nem toda entidade pode ser adicionada, nem toda entidade possui um repositório. As consultas variam muito; a API do repositório se torna tão exclusiva quanto a própria entidade.

Um padrão que costumo usar é ter interfaces de repositório específicas, mas uma classe base para as implementações. Por exemplo, usando LINQ to SQL, você pode fazer:

public abstract class Repository<TEntity>
{
    private DataContext _dataContext;

    protected Repository(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    protected IQueryable<TEntity> Query
    {
        get { return _dataContext.GetTable<TEntity>(); }
    }

    protected void InsertOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().InsertOnCommit(entity);
    }

    protected void DeleteOnCommit(TEntity entity)
    {
        _dataContext.GetTable<TEntity>().DeleteOnCommit(entity);
    }
}

Substitua DataContextpor sua unidade de trabalho de sua escolha. Um exemplo de implementação pode ser:

public interface IUserRepository
{
    User GetById(int id);

    IQueryable<User> GetLockedOutUsers();

    void Insert(User user);
}

public class UserRepository : Repository<User>, IUserRepository
{
    public UserRepository(DataContext dataContext) : base(dataContext)
    {}

    public User GetById(int id)
    {
        return Query.Where(user => user.Id == id).SingleOrDefault();
    }

    public IQueryable<User> GetLockedOutUsers()
    {
        return Query.Where(user => user.IsLockedOut);
    }

    public void Insert(User user)
    {
        InsertOnCommit(user);
    }
}

Observe que a API pública do repositório não permite que os usuários sejam excluídos. Além disso, a exposição IQueryableé uma outra lata de minhocas - existem tantas opiniões quanto umbigos sobre esse tópico.

Bryan Watts
fonte
9
Sério, ótima resposta. Obrigado!
Beep beep
5
Então, como você usaria IoC / DI com isso? (Eu sou um novato na IoC) Minha pergunta em relação ao seu padrão na íntegra: stackoverflow.com/questions/4312388/…
e dan
36
"um repositório é uma parte do domínio que está sendo modelado, e esse domínio não é genérico. Nem toda entidade pode ser excluída, nem toda entidade pode ser adicionada, nem toda entidade possui um repositório" perfeito!
Adamwtiko
Eu sei que esta é uma resposta antiga, mas estou curioso para deixar de fora um método Update da classe Repository com intencional. Estou tendo problemas para encontrar uma maneira limpa de fazer isso.
Rtf
1
Oldie, mas um goodie. Este post é incrivelmente sábio e deve ser lido e relido, mas todos os desenvolvedores bem-sucedidos. Obrigado @BryanWatts. Minha implementação é tipicamente baseada em dapper, mas a premissa é a mesma. Repositório base, com repositórios específicos para representar o domínio que aceita os recursos.
Pimbrouwers
27

Na verdade, eu discordo um pouco do post de Bryan. Acho que ele está certo, que no final tudo é muito único e assim por diante. Mas, ao mesmo tempo, a maior parte disso sai à medida que você projeta, e eu acho que, ao criar um repositório genérico e usá-lo durante o desenvolvimento do meu modelo, posso instalar um aplicativo muito rapidamente, e refatorar para uma maior especificidade, pois precisa fazer isso.

Portanto, em casos como esse, muitas vezes criei um IRepository genérico que possui a pilha CRUD completa, e isso permite que eu rapidamente jogue com a API e deixe que as pessoas joguem com a interface do usuário e faça testes de integração e aceitação do usuário em paralelo. Então, como acho que preciso de consultas específicas no repositório, etc, começo a substituir essa dependência pelo específico, se necessário, e a partir daí. Um impl impl. é fácil de criar e usar (e possivelmente conectar-se a um banco de dados na memória ou a objetos estáticos ou objetos simulados ou o que for).

Dito isto, o que comecei a fazer ultimamente é acabar com o comportamento. Portanto, se você fizer interfaces para IDataFetcher, IDataUpdater, IDataInserter e IDataDeleter (por exemplo), poderá combinar e combinar para definir seus requisitos por meio da interface e depois ter implementações que cuidam de alguns ou de todos eles, e eu posso ainda injeto a implementação de tudo para usar enquanto estou criando o aplicativo.

Paulo

Paulo
fonte
4
Obrigado pela resposta @Paul. Na verdade, eu tentei essa abordagem também. Não consegui descobrir como expressar genericamente o primeiro método que tentei GetById(),. Devo usar IRepository<T, TId>, GetById(object id)ou fazer suposições e usar GetById(int id)? Como as chaves compostas funcionariam? Gostaria de saber se uma seleção genérica por ID era uma abstração que valeria a pena. Caso contrário, o que mais os repositórios genéricos seriam forçados a expressar de maneira desajeitada? Essa foi a linha de raciocínio por trás da abstração da implementação , não da interface .
perfil completo de Bryan Watts
10
Além disso, um mecanismo de consulta genérico é de responsabilidade de um ORM. Seus repositórios devem implementar as consultas específicas para as entidades do seu projeto usando o mecanismo de consulta genérico. Os consumidores do seu repositório não devem ser forçados a escrever suas próprias consultas, a menos que isso faça parte do domínio do problema, como nos relatórios.
Bryan Watts
2
@Bryan - Em relação ao seu GetById (). Eu uso um FindById <T, TId> (ID de ID); Assim, resultando em algo como repository.FindById <Invoice, int> (435);
Joshua Hayes
Normalmente, não uso métodos de consulta em nível de campo nas interfaces genéricas, francamente. Como você apontou, nem todos os modelos devem ser consultados por uma única chave e, em alguns casos, seu aplicativo nunca recuperará algo por um ID (por exemplo, se você estiver usando chaves primárias geradas por banco de dados e buscar apenas por uma chave natural, por exemplo, nome de login). Os métodos de consulta evoluem nas interfaces específicas que eu construo como parte da refatoração.
Paul
13

Prefiro repositórios específicos derivados de repositórios genéricos (ou lista de repositórios genéricos para especificar o comportamento exato) com assinaturas de método substituíveis.

Arnis Lapsa
fonte
Você poderia fornecer um pequeno exemplo de snippet-ish?
Johann Gerell
@ John Gerell não, porque eu não uso mais repositórios.
Arnis Lapsa
o que você usa agora que fica longe dos repositórios?
31511 Chris
@ Chris me concentro fortemente em ter um modelo de domínio rico. parte importante da aplicação é o que é importante. se todas as alterações de estado forem monitoradas com cuidado, não importa o quanto você lê os dados, desde que sejam suficientemente eficientes. então eu apenas uso o NHibernate ISession diretamente. sem a abstração da camada do repositório, é muito mais fácil especificar coisas como carregamento rápido, várias consultas etc. e se você realmente precisa, não é difícil zombar do ISession também.
Arnis Lapsa 31/03
3
@JesseWebb Naah ... Com um modelo de domínio rico, a lógica de consulta é simplificada significativamente. Por exemplo, se eu quiser procurar usuários que compraram algo, eu apenas procuro users.Where (u => u.HasPurchasedAnything) em vez de users.Join (x => Orders, algo que eu não conheço linq) .Where ( ordem => order.Status == 1) .join (x => x.products) .Where (x, etc .... etc etc .... blablablá
Arnis Lapsa
5

Tenha um repositório genérico envolvido por um repositório específico. Dessa forma, você pode controlar a interface pública, mas ainda tem a vantagem da reutilização de código proveniente de um repositório genérico.

Peter
fonte
2

classe pública UserRepository: Repository, IUserRepository

Você não deve injetar IUserRepository para evitar a exposição da interface. Como as pessoas disseram, você pode não precisar da pilha CRUD completa etc.

Ste
fonte
2
Parece mais um comentário do que uma resposta.
Amit Joshi