web-dev-qa-db-fra.com

Avantage de créer un référentiel générique par rapport à un référentiel spécifique pour chaque objet?

Nous développons une application ASP.NET MVC et construisons maintenant les classes de référentiel/service. Je me demande s'il y a des avantages majeurs à créer une interface générique IRepository que tous les référentiels implémentent, par rapport à chaque référentiel ayant sa propre interface unique et un ensemble de méthodes.

Par exemple: une interface générique IRepository pourrait ressembler à (tirée de cette réponse ):

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

Chaque référentiel implémenterait cette interface, par exemple:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • etc.

L'alternative que nous avons suivie dans les projets précédents serait:

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

Dans le second cas, les expressions (LINQ ou autre) seraient entièrement contenues dans l'implémentation du référentiel, celui qui implémente le service a juste besoin de savoir quelle fonction de référentiel appeler.

Je suppose que je ne vois pas l'avantage d'écrire toute la syntaxe d'expression dans la classe de service et de passer au référentiel. Cela ne signifie-t-il pas que le code LINQ facile à gérer est dupliqué dans de nombreux cas?

Par exemple, dans notre ancien système de facturation, nous appelons

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

à partir de quelques services différents (client, facture, compte, etc.). Cela semble beaucoup plus propre que d'écrire ce qui suit à plusieurs endroits:

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

Le seul inconvénient que je vois à utiliser l'approche spécifique est que nous pourrions nous retrouver avec de nombreuses permutations des fonctions Get *, mais cela semble toujours préférable à pousser la logique d'expression vers le haut dans les classes Service.

Qu'est-ce que je rate?

130
Beep beep

Il s'agit d'un problème aussi ancien que le modèle de référentiel lui-même. L'introduction récente de LINQ IQueryable, une représentation uniforme d'une requête, a suscité beaucoup de discussions sur ce sujet.

Je préfère moi-même des référentiels spécifiques, après avoir travaillé très dur pour construire un référentiel générique. Quel que soit le mécanisme intelligent que j'ai essayé, je me retrouvais toujours au même problème: un référentiel fait partie du domaine en cours de modélisation, et ce domaine n'est pas générique. Toutes les entités ne peuvent pas être supprimées, toutes les entités ne peuvent pas être ajoutées, toutes les entités n'ont pas de référentiel. Les requêtes varient énormément; l'API du référentiel devient aussi unique que l'entité elle-même.

Un modèle que j'utilise souvent est d'avoir des interfaces de référentiel spécifiques, mais une classe de base pour les implémentations. Par exemple, en utilisant LINQ to SQL, vous pouvez faire:

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

Remplacez DataContext par votre unité d'oeuvre de choix. Un exemple d'implémentation pourrait être:

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

Notez que l'API publique du référentiel ne permet pas de supprimer des utilisateurs. En outre, exposer IQueryable est une toute autre boîte de vers - il y a autant d'opinions que de boutons de nombril sur ce sujet.

164
Bryan Watts

Je suis en fait légèrement en désaccord avec le post de Bryan. Je pense qu'il a raison, qu'en fin de compte, tout est unique et ainsi de suite. Mais en même temps, la plupart de ces informations apparaissent au fur et à mesure que vous concevez, et je trouve qu'en obtenant un référentiel générique et en l'utilisant lors du développement de mon modèle, je peux obtenir une application très rapidement, puis refactoriser pour une plus grande spécificité lorsque je trouve le besoin de le faire.

Donc, dans des cas comme ça, j'ai souvent créé un IRepository générique qui a la pile CRUD complète, et qui me permet de jouer rapidement avec l'API et de laisser les gens jouer avec l'interface utilisateur et faire des tests d'intégration et d'acceptation des utilisateurs en parallèle. Ensuite, comme je trouve que j'ai besoin de requêtes spécifiques sur le référentiel, etc., je commence à remplacer cette dépendance avec la spécifique si nécessaire et à partir de là. Un impl. Sous-jacent. est facile à créer et à utiliser (et peut-être à accrocher à une base de données en mémoire ou à des objets statiques ou à des objets simulés ou autre).

Cela dit, ce que j'ai commencé à faire récemment, c'est de briser le comportement. Donc, si vous faites des interfaces pour IDataFetcher, IDataUpdater, IDataInserter et IDataDeleter (par exemple), vous pouvez mélanger et assortir pour définir vos besoins via l'interface, puis avoir des implémentations qui prennent en charge une partie ou la totalité d'entre elles, et je peux injectez toujours l'implémentation à faire pendant que je crée l'application.

paul

26
Paul

Je préfère des référentiels spécifiques qui dérivent d'un référentiel générique (ou d'une liste de référentiels génériques pour spécifier le comportement exact) avec des signatures de méthode remplaçables.

12
Arnis Lapsa

Avoir un référentiel générique enveloppé par un référentiel spécifique. De cette façon, vous pouvez contrôler l'interface publique tout en bénéficiant de la réutilisation de code qui provient d'un référentiel générique.

5
Peter

classe publique UserRepository: Repository, IUserRepository

Ne devriez-vous pas injecter IUserRepository pour éviter d'exposer l'interface. Comme les gens l'ont dit, vous n'aurez peut-être pas besoin de la pile CRUD complète, etc.

1
Ste