C'è un vero vantaggio nel repository generico?


28

Stavo leggendo alcuni articoli sui vantaggi della creazione di repository generici per una nuova app ( esempio ). L'idea sembra carina perché mi consente di utilizzare lo stesso repository per fare diverse cose per diversi tipi di entità contemporaneamente:

IRepository repo = new EfRepository(); // Would normally pass through IOC into constructor 
var c1 = new Country() { Name = "United States", CountryCode = "US" };
var c2 = new Country() { Name = "Canada", CountryCode = "CA" };
var c3 = new Country() { Name = "Mexico", CountryCode = "MX" };
var p1 = new Province() { Country = c1, Name = "Alabama", Abbreviation = "AL" };
var p2 = new Province() { Country = c1, Name = "Alaska", Abbreviation = "AK" };
var p3 = new Province() { Country = c2, Name = "Alberta", Abbreviation = "AB" };
repo.Add<Country>(c1);
repo.Add<Country>(c2);
repo.Add<Country>(c3);
repo.Add<Province>(p1);
repo.Add<Province>(p2);
repo.Add<Province>(p3);
repo.Save();

Tuttavia, il resto dell'implementazione del repository dipende fortemente da Linq:

IQueryable<T> Query();
IList<T> Find(Expression<Func<T,bool>> predicate);
T Get(Expression<Func<T,bool>> predicate);
T First(Expression<Func<T,bool>> predicate);
//... and so on

Questo modello di repository ha funzionato alla grande per Entity Framework e praticamente ha offerto una mappatura da 1 a 1 dei metodi disponibili su DbContext / DbSet. Ma dato il lento assorbimento di Linq su altre tecnologie di accesso ai dati al di fuori di Entity Framework, quale vantaggio offre questo rispetto al lavorare direttamente con DbContext?

Ho tentato di scrivere una versione PetaPoco del repository, ma PetaPoco non supporta Linq Expressions, il che rende la creazione di un'interfaccia IRepository generica praticamente inutile a meno che non venga utilizzata solo per GetAll, GetById, Aggiungi, Aggiorna, Elimina e Salva metodi e utilizzarlo come una classe di base. Quindi devi creare repository specifici con metodi specializzati per gestire tutte le clausole "where" che in precedenza avrei potuto passare come predicato.

Il modello di repository generico è utile per qualsiasi cosa al di fuori di Entity Framework? In caso contrario, perché qualcuno dovrebbe usarlo affatto invece di lavorare direttamente con Entity Framework?


Il link originale non riflette lo schema che stavo usando nel mio codice di esempio. Ecco un ( link aggiornato ).



1
La domanda è davvero "advatange del repository generico" o è più "come nascondere query complesse dietro un'interfaccia del repository generico"? È possibile essere generici se la sua interfaccia e il suo utilizzo dipendono da linq? Il nostro Repositoryapi ha un metodo QueryByExample che è completamente indipendente dalla tecnica di ricerca e consentirebbe di spostare l'implementazione.
k3b,

"Mi permette di usare lo stesso repository per fare diverse cose per diversi tipi di entità" => Sono io o hai semplicemente frainteso che cos'è un repository generico (incluso per quanto riguarda l'articolo citato)? Per me, un repository generico ha sempre significato templare tutti i repository con una singola classe o interfaccia, non avere un'unica istanza di repository che gestisce la persistenza di tutte le entità qualunque sia il loro tipo ...
guillaume31

Risposte:


35

Il repository generico è persino inutile (e anche IMHO non valido) per Entity Framework. Non apporta alcun valore aggiuntivo a ciò che è già fornito da IDbSet<T>(che è tra repository generico).

Come hai già scoperto, l'argomento secondo cui il repository generico può essere sostituito dall'implementazione per altre tecnologie di accesso ai dati è piuttosto debole perché può richiedere la scrittura del proprio provider Linq.

Anche il secondo argomento comune sul test unitario semplificato è sbagliato perché il repository / set beffardo con l'archiviazione dei dati in memoria sostituisce il provider Linq con un altro con funzionalità diverse. Il provider Linq-to-entity supporta solo un sottoinsieme delle funzionalità di Linq - non supporta nemmeno tutti i metodi disponibili IQueryable<T>sull'interfaccia. La condivisione degli alberi delle espressioni tra il livello di accesso ai dati e i livelli della logica aziendale impedisce qualsiasi falsificazione del livello di accesso ai dati: la logica della query deve essere separata.

Se vuoi avere una forte astrazione "generica" ​​devi coinvolgere anche altri schemi. In questo caso è necessario utilizzare un linguaggio di query astratto che può essere tradotto dal repository in un linguaggio di query specifico supportato dal livello di accesso ai dati utilizzato. Questo è gestito dal modello di specifica. Linq on IQueryableè una specifica (ma la traduzione richiede provider - o qualche visitatore personalizzato che traduce l'albero delle espressioni in query) ma è possibile definire la propria versione semplificata e usarla. Ad esempio, NHibernate utilizza l'API dei criteri. Ancora il modo più semplice è usare repository specifici con metodi specifici. In questo modo è il più semplice da implementare, il più semplice da testare e il più semplice da falsificare nei test unitari perché la logica della query è completamente nascosta e separata dietro l'astrazione.


Solo una domanda veloce, NHibernate ha quello ISessionche è facilmente beffardo per scopi di test di unità generali, e lascerei volentieri il 'repository' anche quando lavoro con EF - ma c'è un modo più semplice e diretto per ricrearlo? Qualcosa di simile a ISessionquello ISessionFactory, perché non c'è niente IDbContextche io possa dire ...
Patryk Ćwiek

No non c'è IDbContext- se vuoi IDbContextpuoi semplicemente crearne uno e implementarlo nel tuo contesto derivato.
Ladislav Mrnka,

Quindi stai dicendo che per le app MVC di tutti i giorni, IDbSet <T> dovrebbe fornire un repository generico abbastanza buono da dimenticare completamente il modello di repository. Tranne situazioni speciali, ad es. In cui non è consentito alcun riferimento DAL nel progetto MVC.
ProfK

1
Buona risposta. Alcuni sviluppatori creano solo un altro livello di astrazioni senza alcun motivo. IMO, EF non necessitano di repository generico né di un'unità di lavoro (sono
integrati

1
IDbSet <T> è un repository generico con una dipendenza da Entity Framework che tipo di sconfigge lo scopo di utilizzare un repository generico per sottrarre la dipendenza da Entity Framework.
Joel McBeth,

7

Il problema non è il modello di repository. Avere un'astrazione tra ottenere dati e come li stai ottenendo è una buona cosa.

Il problema qui è l'implementazione. Supporre che un'espressione arbitraria funzionerà per il filtraggio è nella migliore delle ipotesi.

Fare in modo che un repository funzioni per tutti i tuoi oggetti direttamente non riesce a capire il punto. Gli oggetti dati raramente verranno mai mappati direttamente agli oggetti business. Passare in T per filtrare ha molto meno senso in queste situazioni. E fornire tale funzionalità in gran parte garantisce che non puoi supportarla tutta una volta che arriva un fornitore diverso.


Quindi ha più senso avere un repository per oggetto dati o un gruppo di oggetti strettamente correlati, con metodi specifici come GetById (int id), SortedList (), ecc.? Sto restituendo elenchi di oggetti dati da un repository o li sto trasformando in oggetti business con anche i campi necessari? Kinda pensava che fosse quello che era successo nel livello Service / Business.
Sam,

1
@sam - Tendo a preferire repository più specifici, sì. Se stanno facendo traduzioni o no dipende da dove è probabile che le cose cambino. Se il tuo dominio è ben definito, restituirei le cose nella forma del dominio. Se i dati sono ben definiti, restituirei le cose nella forma delle strutture dei dati. Se nessuno dei due lo fosse, avrei un'entità che funge da base ben definita per costruire e poi adattarsi a / da quello.
Telastyn,

1
Non c'è motivo per cui non si possano avere repository specifici che delegano le cose comuni a un repository generico, ma hanno anche metodi specifici specifici di repository per chiamate più complesse. L'ho visto fare così diverse volte.
Eric King,

@EricKing Ho anche io, ed è stato buono lì, ma tende a perdere qualche astrazione poiché le cose comuni tendono ad esistere solo a causa della comunanza di come sono archiviati i dati (GetByID ad esempio richiede tabelle con ID).
Telastyn,

@Telastyn Sì, sono d'accordo, ma succede anche con repository specifici. Le astrazioni che perdono non sono specifiche dei repository generici. (Caspita, avrei potuto dirlo in modo più goffo?)
Eric King,

2

Il valore di un livello dati generico (un repository è un tipo particolare di livello dati) consente al codice di modificare il meccanismo di archiviazione sottostante con un impatto minimo o nullo sul codice chiamante.

In teoria, funziona bene. In pratica, come hai osservato, l'astrazione spesso perde. I meccanismi utilizzati per accedere ai dati in uno sono diversi dai meccanismi in un altro. In alcuni casi, si finisce per scrivere il codice due volte: una volta nel livello aziendale e ripetendolo nel livello dati.

Il modo più efficace per creare un livello dati generico è conoscere i diversi tipi di origini dati che l'applicazione utilizzerà in anticipo. Come hai visto, supporre che LINQ o SQL sia universale può essere un problema. Cercare di aggiornare nuovi archivi di dati comporterà probabilmente una riscrittura.

[Modifica: aggiunto quanto segue.]

Dipende anche dalle esigenze dell'applicazione dal livello dati. Se l'applicazione sta caricando o archiviando oggetti, il livello dati può essere molto semplice. Man mano che aumenta la necessità di cercare, ordinare e filtrare, la complessità del livello dati aumenta e le astrazioni iniziano a perdere (come esporre le query LINQ nella domanda). Una volta che gli utenti possono fornire le proprie query, tuttavia, è necessario valutare attentamente il costo / beneficio del livello dati.


1

Avere un livello di codice sopra il database è utile in quasi tutti i casi. Preferirei in genere un modello "GetByXXXXX" in detto codice: ti consente di ottimizzare le query dietro di esso secondo necessità, mantenendo l'interfaccia utente libera dal disordine dell'interfaccia dati.

Sfruttare i farmaci generici è sicuramente un gioco equo: avere un Load<T>(int id)metodo ha molto senso. Ma la creazione di repository attorno a LINQ è l'equivalente del 2010 del rilascio di query sql ovunque con un po 'di sicurezza in più.


0

Bene, con il link fornito posso vedere che può essere un comodo wrapper per a DataServiceContext, ma non riduce le manipolazioni del codice né migliora la leggibilità. Inoltre, l'accesso a DataServiceQuery<T>è ostruito, limitando la flessibilità a .Where()e .Single(). Né sono AddRange()o alternative fornite. Né viene Delete(Predicate)fornito che potrebbe essere utile ( repo.Delete<Person>( p => p.Name=="Joe" );per eliminare Joe-s). Eccetera.

Conclusione: tale API ostruisce l'API nativa e la limita a poche semplici operazioni.


Hai ragione. Quello non era l'articolo che utilizzava il modello che ho nel mio codice di esempio. Proverò a trovare il link quando torno a casa.
Sam,

Collegamento aggiornato aggiunto alla fine della domanda.
Sam,

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.