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

132

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 IRepository générique que tous les référentiels implémentent, par rapport à chaque référentiel ayant sa propre interface et un ensemble de méthodes uniques.

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, quiconque 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 la transmettre au référentiel. Cela ne signifierait-il pas que du code LINQ facile à manipuler 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 à l'utilisation de l'approche spécifique est que nous pourrions nous retrouver avec de nombreuses permutations de fonctions Get *, mais cela semble toujours préférable à pousser la logique de l'expression dans les classes Service.

Qu'est-ce que je rate?

Bip Bip
la source
L'utilisation de référentiels génériques avec des ORM complets semble inutile. J'en ai discuté en détail ici .
Amit Joshi

Réponses:

169

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 provoqué de nombreuses discussions sur ce sujet même.

Je préfère moi-même les référentiels spécifiques, après avoir travaillé très dur pour construire un framework de référentiel générique. Peu importe le mécanisme intelligent que j'ai essayé, je me suis toujours retrouvé 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 ne disposent pas d'un 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-la DataContextpar l'unité de travail de votre choix. Un exemple de mise en œuvre 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 n'autorise pas la suppression des utilisateurs. En outre, exposer IQueryableest une toute autre boîte de vers - il y a autant d'opinions que de nombril sur ce sujet.

Bryan Watts
la source
9
Sérieusement, bonne réponse. Merci!
Bip bip
5
Alors, comment utiliseriez-vous IoC / DI avec cela? (Je suis novice à l'IoC) Ma question concernant votre modèle dans son intégralité: stackoverflow.com/questions/4312388/...
dan
36
"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" parfait!
adamwtiko
Je sais que c'est une vieille réponse, mais je suis curieux de savoir si une méthode Update de la classe Repository est omise avec intentionnelle. J'ai du mal à trouver un moyen propre de le faire.
rtf le
1
Oldie mais un goodie. Cet article est incroyablement sage et devrait être lu et relu, mais tous les développeurs aisés. Merci @BryanWatts. Ma mise en œuvre est généralement basée sur pimpant, mais le principe est le même. Référentiel de base, avec des référentiels spécifiques pour représenter le domaine qui accepte les fonctionnalités.
pimbrouwers
27

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

Donc, dans des cas comme celui-là, 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 dépôt, etc., je commence à remplacer cette dépendance par la dépendance spécifique si nécessaire et à partir de là. Un impl sous-jacent. est facile à créer et à utiliser (et peut-être accroché à 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 ces derniers temps, 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 tout ou partie d'entre eux, et je peux injectez toujours l'implémentation do-it-all à utiliser pendant que je construis l'application.

Paul

Paul
la source
4
Merci pour la réponse @Paul. J'ai également essayé cette approche. Je ne pouvais pas comprendre comment exprimer de manière générique la toute première méthode que j'ai essayée GetById(). Dois-je utiliser IRepository<T, TId>, GetById(object id)ou faire des hypothèses et utiliser GetById(int id)? Comment fonctionneraient les clés composites? Je me suis demandé si une sélection générique par ID était une abstraction valable. Sinon, qu'est-ce que les référentiels génériques seraient obligés d'exprimer à la légère? C'était le raisonnement derrière l'abstraction de l' implémentation , pas de l' interface .
Bryan Watts
10
En outre, un mécanisme de requête générique relève de la responsabilité d'un ORM. Vos référentiels doivent implémenter les requêtes spécifiques pour les entités de votre projet à l' aide du mécanisme de requête générique. Les consommateurs de votre référentiel ne doivent pas être obligés d'écrire leurs propres requêtes, sauf si cela fait partie de votre domaine problématique, par exemple avec la création de rapports.
Bryan Watts du
2
@Bryan - Concernant votre GetById (). J'utilise un FindById <T, TId> (ID TId); Résultat: quelque chose comme repository.FindById <Invoice, int> (435);
Joshua Hayes
Je ne mets généralement pas de méthodes de requête au niveau du champ sur les interfaces génériques, franchement. Comme vous l'avez souligné, tous les modèles ne doivent pas être interrogés par une seule clé, et dans certains cas, votre application ne récupérera jamais quelque chose par un ID (par exemple, si vous utilisez des clés primaires générées par DB et que vous ne récupérez que par une clé naturelle, par exemple un nom de connexion). Les méthodes de requête évoluent sur les interfaces spécifiques que je construis dans le cadre du refactoring.
Paul
13

Je préfère les 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.

Arnis Lapsa
la source
Pourriez-vous s'il vous plaît fournir un petit exemple d'extrait de code?
Johann Gerell
@Johann Gerell non, car je n'utilise plus de référentiels.
Arnis Lapsa
qu'utilisez-vous maintenant que vous vous éloignez des référentiels?
Chris
@Chris je me concentre fortement sur le modèle de domaine riche. la partie d'entrée de l'application est ce qui est important. si tous les changements d'état sont soigneusement surveillés, peu importe la façon dont vous lisez les données tant qu'elles sont suffisamment efficaces. donc j'utilise juste NHibernate ISession directement. sans abstraction de la couche de référentiel, il est beaucoup plus facile de spécifier des éléments tels que le chargement hâtif, les requêtes multiples, etc. et si vous en avez vraiment besoin, il n'est pas difficile de se moquer de ISession aussi.
Arnis Lapsa
3
@JesseWebb Naah ... Avec un modèle de domaine riche, la logique de requête est considérablement simplifiée. Par exemple, si je veux rechercher des utilisateurs qui ont acheté quelque chose, je recherche simplement des utilisateurs.Where (u => u.HasPurchasedAnything) au lieu d'utilisateurs.Join (x => Commandes, quelque chose, je ne sais pas linq). order => order.Status == 1) .Join (x => x.products) .Where (x .... etc etc etc .... blah blah blah
Arnis Lapsa
5

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 conservant l'avantage de la réutilisation du code qui provient d'un référentiel générique.

Peter
la source
2

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.

Ste
la source
2
Cela ressemble plus à un commentaire qu'à une réponse.
Amit Joshi