Vorteil der Erstellung eines generischen Repositorys gegenüber einem spezifischen Repository für jedes Objekt?

132

Wir entwickeln eine ASP.NET MVC-Anwendung und erstellen jetzt die Repository- / Serviceklassen. Ich frage mich, ob die Erstellung einer generischen IRepository-Schnittstelle, die von allen Repositorys implementiert wird, wesentliche Vorteile bietet, während jedes Repository über eine eigene Schnittstelle und einen eigenen Methodensatz verfügt.

Beispiel: Eine generische IRepository-Schnittstelle könnte folgendermaßen aussehen (aus dieser Antwort entnommen ):

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

Jedes Repository würde diese Schnittstelle implementieren, zum Beispiel:

  • CustomerRepository: IRepository
  • ProductRepository: IRepository
  • etc.

Die Alternative, der wir in früheren Projekten gefolgt sind, wäre:

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

Im zweiten Fall wären die Ausdrücke (LINQ oder anderweitig) vollständig in der Repository-Implementierung enthalten. Wer auch immer den Service implementiert, muss nur wissen, welche Repository-Funktion aufgerufen werden soll.

Ich glaube, ich sehe nicht den Vorteil, die gesamte Ausdruckssyntax in die Serviceklasse zu schreiben und an das Repository zu übergeben. Würde dies nicht bedeuten, dass in vielen Fällen leicht zu verwirrender LINQ-Code dupliziert wird?

In unserem alten Rechnungsstellungssystem rufen wir beispielsweise an

InvoiceRepository.GetSingleInvoice(DateTime invoiceDate, int accountId)

von einigen verschiedenen Diensten (Kunde, Rechnung, Konto usw.). Das scheint viel sauberer zu sein, als an mehreren Stellen Folgendes zu schreiben:

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

Der einzige Nachteil, den ich bei der Verwendung des spezifischen Ansatzes sehe, besteht darin, dass wir möglicherweise viele Permutationen von Get * -Funktionen erhalten, dies scheint jedoch immer noch vorzuziehen, um die Ausdruckslogik in die Service-Klassen zu verschieben.

Was vermisse ich?

Piep Piep
quelle
Die Verwendung generischer Repositorys mit vollständigen ORMs erscheint nutzlos. Ich habe dies hier ausführlich besprochen .
Amit Joshi

Antworten:

169

Dies ist ein Problem, das so alt ist wie das Repository-Muster. Die kürzlich erfolgte Einführung von LINQs IQueryable, einer einheitlichen Darstellung einer Abfrage, hat viele Diskussionen zu diesem Thema ausgelöst.

Ich bevorzuge selbst bestimmte Repositorys, nachdem ich sehr hart daran gearbeitet habe, ein generisches Repository-Framework zu erstellen. Egal welchen cleveren Mechanismus ich ausprobiert habe, ich hatte immer das gleiche Problem: Ein Repository ist Teil der zu modellierenden Domäne, und diese Domäne ist nicht generisch. Nicht jede Entität kann gelöscht werden, nicht jede Entität kann hinzugefügt werden, nicht jede Entität verfügt über ein Repository. Abfragen variieren stark; Die Repository-API wird so eindeutig wie die Entität selbst.

Ein Muster, das ich häufig verwende, besteht darin, bestimmte Repository-Schnittstellen zu haben, aber eine Basisklasse für die Implementierungen. Mit LINQ to SQL können Sie beispielsweise Folgendes tun:

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

Durch eine DataContextArbeitseinheit Ihrer Wahl ersetzen . Eine beispielhafte Implementierung könnte sein:

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

Beachten Sie, dass die öffentliche API des Repositorys das Löschen von Benutzern nicht zulässt. Das Aussetzen IQueryableist auch eine ganz andere Dose Würmer - es gibt so viele Meinungen wie Bauchnabel zu diesem Thema.

Bryan Watts
quelle
9
Im Ernst, tolle Antwort. Vielen Dank!
Beep Beep
5
Wie würden Sie IoC / DI damit verwenden? (Ich bin ein Neuling bei IoC) Meine Frage in Bezug auf Ihr Muster vollständig: stackoverflow.com/questions/4312388/…
dan
36
"Ein Repository ist ein Teil der zu modellierenden Domäne, und diese Domäne ist nicht generisch. Nicht jede Entität kann gelöscht werden, nicht jede Entität kann hinzugefügt werden, nicht jede Entität hat ein Repository" perfekt!
Adamwtiko
Ich weiß, dass dies eine alte Antwort ist, aber ich bin neugierig, ob ich absichtlich eine Update-Methode aus der Repository-Klasse weglasse. Ich habe Probleme, einen sauberen Weg zu finden, um dies zu tun.
RTF
1
Oldie aber ein Goodie. Dieser Beitrag ist unglaublich weise und sollte gelesen und erneut gelesen werden, aber alle wohlhabenden Entwickler. Danke @BryanWatts. Meine Implementierung basiert normalerweise auf Dapper, aber die Prämisse ist dieselbe. Basis-Repository mit spezifischen Repositorys zur Darstellung der Domäne, die sich für Features anmeldet.
Pimbrouwers
27

Ich bin eigentlich ein wenig anderer Meinung als Bryans Beitrag. Ich denke er hat Recht, dass letztendlich alles sehr einzigartig ist und so weiter. Gleichzeitig kommt das meiste davon beim Entwerfen heraus, und ich stelle fest, dass ich, wenn ich ein generisches Repository einrichten und es während der Entwicklung meines Modells verwenden kann, sehr schnell eine App einrichten und sie dann genauer umgestalten kann, wenn ich das finde müssen dies tun.

In solchen Fällen habe ich häufig ein generisches IRepository erstellt, das über den vollständigen CRUD-Stack verfügt und das es mir ermöglicht, schnell mit der API zu spielen und die Leute mit der Benutzeroberfläche spielen zu lassen und parallel Integrations- und Benutzerakzeptanztests durchzuführen. Wenn ich dann feststelle, dass ich bestimmte Abfragen für das Repo usw. benötige, beginne ich, diese Abhängigkeit bei Bedarf durch die spezifische zu ersetzen und gehe von dort aus. Ein zugrunde liegendes impl. ist einfach zu erstellen und zu verwenden (und möglicherweise mit einer speicherinternen Datenbank oder statischen Objekten oder verspotteten Objekten oder was auch immer zu verknüpfen).

Das heißt, was ich in letzter Zeit angefangen habe, ist das Verhalten zu brechen. Wenn Sie also Schnittstellen für IDataFetcher, IDataUpdater, IDataInserter und IDataDeleter (zum Beispiel) erstellen, können Sie Ihre Anforderungen mischen und anpassen, um Ihre Anforderungen über die Schnittstelle zu definieren, und dann Implementierungen haben, die sich um einige oder alle kümmern, und ich kann Fügen Sie immer noch die Allround-Implementierung ein, die Sie verwenden müssen, während ich die App ausbaue.

paul

Paul
quelle
4
Danke für die Antwort @Paul. Ich habe diesen Ansatz auch versucht. Ich konnte nicht herausfinden, wie ich die allererste Methode, die ich versuchte, generisch ausdrücken konnte GetById(). Soll ich IRepository<T, TId>, GetById(object id)oder Annahmen und Verwendung machen GetById(int id)? Wie würden zusammengesetzte Schlüssel funktionieren? Ich fragte mich, ob eine generische Auswahl nach ID eine lohnende Abstraktion war. Wenn nicht, was würden generische Repositories sonst gezwungen sein, akwardly auszudrücken? Das war die Argumentation hinter der Abstraktion der Implementierung , nicht der Schnittstelle .
Bryan Watts
10
Ein generischer Abfragemechanismus liegt ebenfalls in der Verantwortung eines ORM. Ihre Repositorys sollten die spezifischen Abfragen für die Entitäten Ihres Projekts mithilfe des generischen Abfragemechanismus implementieren . Die Konsumenten Ihres Repositorys sollten nicht gezwungen werden, ihre eigenen Abfragen zu schreiben, es sei denn, dies ist Teil Ihrer Problemdomäne, z. B. bei der Berichterstellung.
Bryan Watts
2
@Bryan - In Bezug auf Ihre GetById (). Ich verwende eine FindById <T, TId> (TId-ID). Das Ergebnis ist so etwas wie ein Repository.FindById <Invoice, int> (435);
Joshua Hayes
Ehrlich gesagt, stelle ich normalerweise keine Abfragemethoden auf Feldebene auf die generischen Schnittstellen. Wie Sie bereits betont haben, sollten nicht alle Modelle mit einem einzigen Schlüssel abgefragt werden. In einigen Fällen wird Ihre App niemals etwas über eine ID abrufen (z. B. wenn Sie DB-generierte Primärschlüssel verwenden und nur nach abrufen) ein natürlicher Schlüssel, z. B. Anmeldename). Die Abfragemethoden entwickeln sich auf den spezifischen Schnittstellen, die ich im Rahmen des Refactorings erstellt habe.
Paul
13

Ich bevorzuge bestimmte Repositorys, die vom generischen Repository (oder einer Liste generischer Repositorys zur Angabe des genauen Verhaltens) mit überschreibbaren Methodensignaturen abgeleitet sind.

Arnis Lapsa
quelle
Könnten Sie bitte ein kleines Snippet-Beispiel geben?
Johann Gerell
@Johann Gerell nein, weil ich keine Repositories mehr benutze.
Arnis Lapsa
Was verwenden Sie jetzt, wenn Sie sich von Repositories fernhalten?
Chris
@ Chris Ich konzentriere mich stark auf ein reichhaltiges Domain-Modell. Der Eingabe-Teil der Anwendung ist wichtig. Wenn alle Statusänderungen sorgfältig überwacht werden, spielt es keine Rolle, wie Sie Daten lesen, solange diese effizient genug sind. Also benutze ich NHibernate ISession direkt. Ohne die Abstraktion der Repository-Ebene ist es viel einfacher, Dinge wie eifriges Laden, mehrere Abfragen usw. anzugeben, und wenn Sie es wirklich brauchen, ist es auch nicht schwer, ISession zu verspotten.
Arnis Lapsa
3
@JesseWebb Naah ... Mit Rich Domain Model wird die Abfragelogik erheblich vereinfacht. Wenn ich beispielsweise nach Benutzern suchen möchte, die etwas gekauft haben, suche ich nur nach Benutzern.Wo (u => u.HasPurchasedAnything) anstelle von Benutzern.Join (x => Bestellungen, etwas, das ich nicht kenne linq) .Wo order => order.Status == 1) .Join (x => x.products) .Where (x .... etc etc etc .... bla bla bla
Arnis Lapsa
5

Haben Sie ein generisches Repository, das von einem bestimmten Repository umschlossen wird. Auf diese Weise können Sie die öffentliche Schnittstelle steuern, haben aber dennoch den Vorteil der Wiederverwendung von Code, der aus einem generischen Repository stammt.

Peter
quelle
2

öffentliche Klasse UserRepository: Repository, IUserRepository

Sollten Sie IUserRepository nicht injizieren, um eine Freilegung der Schnittstelle zu vermeiden. Wie bereits erwähnt, benötigen Sie möglicherweise nicht den vollständigen CRUD-Stack usw.

Ste
quelle
2
Dies sieht eher nach einem Kommentar als nach einer Antwort aus.
Amit Joshi