Vantaggio di creare un repository generico rispetto a un repository specifico per ciascun oggetto?


132

Stiamo sviluppando un'applicazione ASP.NET MVC e ora stiamo creando le classi di repository / servizi. Mi chiedo se ci siano dei grandi vantaggi nella creazione di un'interfaccia IRepository generica implementata da tutti i repository, rispetto a ciascun Repository che ha la sua interfaccia unica e una serie di metodi.

Ad esempio: potrebbe apparire un'interfaccia IRepository generica (presa da questa risposta ):

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();
}

Ogni repository implementerebbe questa interfaccia, ad esempio:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • eccetera.

L'alternativa che abbiamo seguito nei progetti precedenti sarebbe:

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);
}

Nel secondo caso, le espressioni (LINQ o altro) sarebbero interamente contenute nell'implementazione del repository, chiunque stia implementando il servizio deve solo sapere quale funzione di repository chiamare.

Immagino di non vedere il vantaggio di scrivere tutta la sintassi delle espressioni nella classe di servizio e passare al repository. Questo non significherebbe che in molti casi il codice LINQ facile da incasinare viene duplicato?

Ad esempio, nel nostro vecchio sistema di fatturazione, chiamiamo

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

da alcuni servizi diversi (Cliente, Fattura, Conto, ecc.). Sembra molto più pulito che scrivere quanto segue in più punti:

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

L'unico svantaggio che vedo nell'uso dell'approccio specifico è che potremmo finire con molte permutazioni delle funzioni Get *, ma questo sembra ancora preferibile spingere la logica di espressione nelle classi di servizio.

Cosa mi sto perdendo?


L'uso di repository generici con ORM completi sembra inutile. Ne ho discusso in dettaglio qui .
Amit Joshi,

Risposte:


169

Questo è un problema vecchio quanto il modello di repository stesso. La recente introduzione di LINQ IQueryable, una rappresentazione uniforme di una query, ha suscitato molte discussioni proprio su questo argomento.

Preferisco io stesso repository specifici, dopo aver lavorato molto duramente per costruire un framework di repository generico. Indipendentemente dal meccanismo intelligente che ho provato, ho sempre avuto lo stesso problema: un repository è una parte del dominio che viene modellato e quel dominio non è generico. Non tutte le entità possono essere eliminate, non tutte le entità possono essere aggiunte, non tutte le entità hanno un repository. Le query variano notevolmente; l'API del repository diventa unica come l'entità stessa.

Un modello che uso spesso è di avere interfacce di repository specifiche, ma una classe base per le implementazioni. Ad esempio, utilizzando LINQ to SQL, è possibile eseguire:

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);
    }
}

Sostituisci DataContextcon la tua unità di lavoro preferita. Un'implementazione di esempio potrebbe essere:

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);
    }
}

Si noti che l'API pubblica del repository non consente l'eliminazione degli utenti. Inoltre, l'esposizione IQueryableè tutta un'altra lattina di vermi - ci sono tante opinioni quanti ombelici su quell'argomento.


9
Seriamente, ottima risposta. Grazie!
Segnale acustico

5
Quindi come useresti IoC / DI con questo? (Sono un novizio all'IoC) La mia domanda per quanto riguarda il tuo modello per intero: stackoverflow.com/questions/4312388/…
dan

36
"un repository è una parte del dominio che viene modellato e quel dominio non è generico. Non tutte le entità possono essere eliminate, non tutte le entità possono essere aggiunte, non tutte le entità hanno un repository" perfetto!
adamwtiko,

So che questa è una vecchia risposta, ma sono curioso di escludere intenzionalmente un metodo di aggiornamento dalla classe Repository. Ho problemi a trovare un modo pulito per farlo.
RTF

1
Vecchio ma un tesoro. Questo post è incredibilmente saggio e dovrebbe essere letto e riletto ma tutti gli sviluppatori benestanti. Grazie @BryanWatts. La mia implementazione è tipicamente basata su dapper, ma la premessa è la stessa. Repository di base, con repository specifici per rappresentare il dominio che attiva le funzionalità.
pimbrouwers,

27

In realtà non sono d'accordo con il post di Bryan. Penso che abbia ragione, che alla fine tutto è davvero unico e così via. Ma allo stesso tempo, la maggior parte di ciò viene fuori mentre progetti, e trovo che ottenere un repository generico e utilizzarlo durante lo sviluppo del mio modello, posso ottenere un'app molto rapidamente, quindi riflettere su una maggiore specificità mentre trovo il bisogno di farlo.

Quindi, in casi del genere, ho spesso creato un IRepository generico che ha lo stack CRUD completo e che mi permette di giocare rapidamente con l'API e di far giocare le persone con l'interfaccia utente e di eseguire in parallelo test di integrazione e accettazione dell'utente. Quindi, poiché trovo che ho bisogno di query specifiche sul repository, ecc., Comincio a sostituire quella dipendenza con quella specifica, se necessario, e passando da lì. Un impl sottostante. è facile da creare e utilizzare (e possibilmente agganciarsi a un db in memoria o oggetti statici o oggetti derisi o altro).

Detto questo, quello che ho iniziato a fare ultimamente è rompere il comportamento. Quindi, se si eseguono interfacce per IDataFetcher, IDataUpdater, IDataInserter e IDataDeleter (ad esempio) è possibile combinare e abbinare per definire i propri requisiti attraverso l'interfaccia e quindi disporre di implementazioni che si occupino di alcuni o tutti loro, e posso iniettare ancora l'implementazione completa da usare mentre sto costruendo l'app.

Paolo


4
Grazie per la risposta @Paul. In realtà ho provato anche questo approccio. Non ho potuto capire come esprimere genericamente il primo metodo che ho provato, GetById(). Dovrei usare IRepository<T, TId>, GetById(object id)o fare ipotesi e usare GetById(int id)? Come funzionerebbero le chiavi composite? Mi chiedevo se una selezione generica per ID fosse un'astrazione utile. In caso contrario, cos'altro i repository generici sarebbero costretti ad esprimere in modo doloroso? Questa era la linea di ragionamento alla base dell'astrazione dell'implementazione , non dell'interfaccia .
Bryan Watts,

10
Inoltre, un meccanismo di query generico è la responsabilità di un ORM. I repository dovrebbero implementare le query specifiche per le entità del progetto utilizzando il meccanismo di query generico. I consumatori del tuo repository non dovrebbero essere costretti a scrivere le proprie query a meno che non faccia parte del dominio del tuo problema, come nel caso della segnalazione.
Bryan Watts,

2
@Bryan - Per quanto riguarda GetById (). Uso un FindById <T, TId> (ID TId); Ne risulta quindi qualcosa come repository.FindById <Invoice, int> (435);
Joshua Hayes,

Di solito non inserisco metodi di query a livello di campo sulle interfacce generiche, francamente. Come hai sottolineato, non tutti i modelli dovrebbero essere interrogati da una singola chiave e in alcuni casi la tua app non recupererà mai nulla tramite un ID (ad esempio se stai usando chiavi primarie generate da DB e recuperi solo da una chiave naturale, ad esempio il nome di accesso). I metodi di query si evolvono sulle interfacce specifiche che costruisco come parte del refactoring.
Paul,

13

Preferisco repository specifici che derivano dal repository generico (o dall'elenco dei repository generici per specificare il comportamento esatto) con firme del metodo sostituibili.


Potresti fornire un piccolo esempio di frammento?
Johann Gerell,

@Johann Gerell no, perché non uso più i repository.
Arnis Lapsa,

cosa usi ora che stai lontano dai repository?
Chris,

@Chris Mi concentro fortemente sull'avere un modello di dominio ricco. parte di input dell'applicazione è ciò che è importante. se tutti i cambiamenti di stato sono attentamente monitorati, non importa molto come leggere i dati purché siano abbastanza efficienti. quindi uso direttamente NHibernate ISession. senza l'astrazione a livello di repository, è molto più facile specificare elementi come il caricamento desideroso, le query multiple, ecc. e se hai davvero bisogno, non è difficile deridere anche ISession.
Arnis Lapsa,

3
@JesseWebb Naah ... Con la ricca logica del modello di dominio la logica delle query viene semplificata in modo significativo. Ad esempio, se voglio cercare utenti che hanno acquistato qualcosa, cerco solo gli utenti. Dove (u => u.HasPurchasedAnything) invece degli utenti.Join (x => Ordini, qualcosa, non so linq) .Where ( order => order.Status == 1) .Join (x => x.products) .Where (x .... etc etc etc .... blah blah blah
Arnis Lapsa

5

Avere un repository generico racchiuso in un repository specifico. In questo modo è possibile controllare l'interfaccia pubblica ma avere comunque il vantaggio di riutilizzare il codice derivante da un repository generico.


2

UserRepository di classe pubblica: Repository, IUserRepository

Non dovresti iniettare IUserRepository per evitare di esporre l'interfaccia. Come hanno detto le persone, potrebbe non essere necessario l'intero stack CRUD ecc.


2
Sembra più un commento che una risposta.
Amit Joshi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.