Alcuni problemi molto evidenti con l'idea di un generico IRepository<T>
:
Si presuppone che ogni entità utilizza lo stesso tipo di chiave, che non è vero in quasi tutti i sistemi non banale. Alcune entità utilizzeranno GUID, altri potrebbero avere una sorta di chiave naturale e/o composita. NHibernate può supportarlo abbastanza bene, ma Linq to SQL è piuttosto scadente: devi scrivere una buona quantità di codice hacker per eseguire la mappatura automatica dei tasti.
Significa che ogni repository può gestire solo un tipo di entità e supporta solo le operazioni più banali. Quando un repository è relegato a un wrapper CRUD così semplice, ha pochissimo uso. Si potrebbe anche solo dare al cliente uno IQueryable<T>
o Table<T>
.
Presuppone che si eseguano esattamente le stesse operazioni su ogni entità. In realtà questo sarà molto lontano dalla verità. Certo, forse vuoi ottenere quello Order
dal suo ID, ma più probabilmente vuoi ottenere un elenco di oggetti Order
per un cliente specifico e entro un certo intervallo di date. La nozione di un numero totale generico di IRepository<T>
non consente il fatto che quasi certamente si desidera eseguire diversi tipi di query su diversi tipi di entità.
Il punto del modello repository è quello di creare un astrazione su modelli di accesso di dati comune. Penso che alcuni programmatori si annoiano con la creazione di repository quindi dicono "Ehi, lo so, creerò un repository über in grado di gestire qualsiasi tipo di entità!" Il che è grande, tranne per il fatto che il repository è praticamente inutile per l'80% di ciò che stai cercando di fare. Va bene come interfaccia di classe base, ma se è tutta la portata del lavoro che fai, sei solo pigro (e garantisci i mal di testa futuri).
Idealmente potrei iniziare con un repository generico che sembra qualcosa di simile:
public interface IRepository<TKey, TEntity>
{
TEntity Get(TKey id);
void Save(TEntity entity);
}
Noterete che questo non lo fa hanno una funzione List
o GetAll
- che è perché è assurdo pensare che sia accettabile recuperare i dati da un'intera tabella in un punto qualsiasi nel codice. Questo è quando hai bisogno di iniziare ad andare in repository specifici:
public interface IOrderRepository : IRepository<int, Order>
{
IEnumerable<Order> GetOrdersByCustomer(Guid customerID);
IPager<Order> GetOrdersByDate(DateTime fromDate, DateTime toDate);
IPager<Order> GetOrdersByProduct(int productID);
}
E così via - hai capito l'idea. In questo modo abbiamo il repository "generico" per se abbiamo davvero bisogno della semantica di recupero per ID incredibilmente semplicistica, ma in generale non lo faremo mai in realtà, certamente non per una classe controller.
Ora, come per i controllori, è necessario fare questo diritto, altrimenti hai praticamente negato tutto il lavoro che hai appena fatto a mettere insieme tutti i repository.
Un controller deve prelevare il proprio repository dal mondo esterno. Il motivo per cui hai creato questi repository è che puoi fare una sorta di Inversion of Control. Il tuo obiettivo finale qui è quello di essere in grado di scambiare un repository con un altro - ad esempio, per eseguire test di unità, o se decidi di passare da Linq a SQL a Entity Framework in futuro.
Un esempio di questo principio è:
public class OrderController : Controller
{
public OrderController(IOrderRepository orderRepository)
{
if (orderRepository == null)
throw new ArgumentNullException("orderRepository");
this.OrderRepository = orderRepository;
}
public ActionResult List(DateTime fromDate, DateTime toDate) { ... }
// More actions
public IOrderRepository OrderRepository { get; set; }
}
In altre parole il controller ha idea di come creare un repository, né deve. Se ci sono delle costruzioni di repository in corso, sta creando un accoppiamento che davvero non vuoi. La ragione per cui i controller di esempio MV.NET ASP.NET hanno costruttori senza parametri che creano repository concreti è che i siti devono essere in grado di compilare ed eseguire senza forzare l'utente a configurare un intero framework Iniezione di dipendenza.
Ma in un sito di produzione, se non si passa la dipendenza del repository tramite un costruttore o una proprietà pubblica, si perde tempo con i repository, perché i controller sono ancora strettamente collegati al livello del database . È necessario essere in grado di scrivere codice di prova in questo modo:
[TestMethod]
public void Can_add_order()
{
OrderController controller = new OrderController();
FakeOrderRepository fakeRepository = new FakeOrderRepository();
controller.OrderRepository = fakeRepository; //<-- Important!
controller.SubmitOrder(...);
Assert.That(fakeRepository.ContainsOrder(...));
}
Non si può fare questo se il vostro OrderController
sta andando fuori e la creazione di un proprio repository. Questo metodo di test non dovrebbe eseguire alcun accesso ai dati, ma si limita a fare in modo che il controller invochi il metodo di repository corretto in base all'azione.
Questo non è ancora DI, si badi, questo è solo finto/beffardo. Dove DI entra nell'immagine è quando decidi che Linq to SQL non sta facendo abbastanza per te e vuoi davvero l'HQL in NHibernate, ma ci vorranno 3 mesi per trasferire tutto e vuoi essere in grado di fai questo un repository alla volta. Così, ad esempio, utilizzando un quadro DI come Ninject, tutto ciò che dovete fare è cambiare questo:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<LinqToSqlOrderRepository>();
Bind<IProductRepository>().To<LinqToSqlProductRepository>();
A:
Bind<ICustomerRepository>().To<LinqToSqlCustomerRepository>();
Bind<IOrderRepository>().To<NHibernateOrderRepository>();
Bind<IProductRepository>().To<NHibernateProductRepository>();
E ci sei tu, ora tutto ciò che dipende da IOrderRepository
sta usando la versione di NHibernate, hai solo dovuto cambiare di una riga di codice anziché potenzialmente centinaia di linee. E stiamo eseguendo le versioni Linq to SQL e NHibernate fianco a fianco, trasferendo funzionalità su un pezzo per pezzo senza mai rompere nulla nel mezzo.
Quindi, per riassumere tutti i punti che ho fatto:
Non fare affidamento unicamente su una generica interfaccia IRepository<T>
. La maggior parte delle funzionalità desiderate da un repository è specifica, non generica. Se vuoi includere uno IRepository<T>
ai livelli superiori della gerarchia di classe/interfaccia, va bene, ma i controllori dovrebbero dipendere dai repository specifici in modo da non dover cambiare il codice in 5 punti diversi quando lo trovi nel repository generico mancano metodi importanti.
I controllori devono accettare i repository dall'esterno, non crearne di propri. Questo è un passo importante per eliminare l'accoppiamento e migliorare la testabilità.
Normalmente si vorrà cablare i controllori usando un framework Iniezione di dipendenza, e molti di essi possono essere perfettamente integrati con ASP.NET MVC.Se questo è troppo per te da accettare, allora almeno dovresti usare un qualche tipo di fornitore di servizi statici in modo da poter centralizzare tutta la logica di creazione del repository. (A lungo termine, probabilmente troverai più semplice imparare e utilizzare un framework DI).
Repository statici generici! SÌÌ! SingletonSquared, proprio quello di cui abbiamo bisogno! ;-) –
@Sky, non si può mai avere abbastanza. Sto pensando personalmente a repository generici statici e parziali. –