it-swarm.com.de

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

Wir entwickeln eine ASP.NET MVC-Anwendung und bauen jetzt die Repository/Service-Klassen auf. Ich frage mich, ob das Erstellen einer generischen IRepository-Schnittstelle, die von allen Repositorys implementiert wird, wesentliche Vorteile hat, im Vergleich dazu, dass jedes Repository eine eigene Schnittstelle und einen Satz von Methoden hat.

Beispiel: Eine generische IRepository-Schnittstelle könnte wie folgt aussehen (entnommen aus diese Antwort ):

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 anders) vollständig in der Repository-Implementierung enthalten. Wer den Service implementiert, muss nur wissen, welche Repository-Funktion aufgerufen werden soll.

Ich glaube, ich sehe keinen Vorteil darin, die gesamte Ausdruckssyntax in der Serviceklasse zu schreiben und an das Repository zu übergeben. Bedeutet dies nicht, dass in vielen Fällen einfach zu messender LINQ-Code dupliziert wird?

In unserem alten Abrechnungssystem 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 Folgendes an mehreren Stellen 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 der Verschiebung der Ausdruckslogik in die Service-Klassen vorzuziehen.

Was vermisse ich?

130
Beep beep

Dies ist ein Problem, das so alt ist wie das Repository-Muster. Die kürzliche Einführung von LINQs IQueryable, einer einheitlichen Darstellung einer Abfrage, hat zu vielen Diskussionen über genau dieses Thema geführt.

Ich bevorzuge bestimmte Repositorys selbst, nachdem ich sehr hart gearbeitet habe, um ein generisches Repository-Framework zu erstellen. Egal, welchen cleveren Mechanismus ich ausprobiert habe, ich bin immer auf das gleiche Problem gestoßen: 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 verfügt über ein Repository. Die Abfragen variieren stark. Die Repository-API wird so eindeutig wie die Entität selbst.

Ein von mir häufig verwendetes Muster 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);
    }
}

Ersetzen Sie DataContext durch Ihre Arbeitseinheit. Eine Beispielimplementierung 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 Entlarven von IQueryable ist auch eine ganze andere Dose von Würmern - es gibt so viele Meinungen wie Bauchnabel zu diesem Thema.

164
Bryan Watts

Ich bin eigentlich ein bisschen anderer Meinung als Bryans Post. Ich denke er hat recht, dass letztendlich alles sehr einzigartig ist und so weiter. Gleichzeitig entsteht das meiste davon beim Entwerfen. Wenn ich ein generisches Repository aufbaue und es bei der Entwicklung meines Modells verwende, kann ich sehr schnell eine App aufbauen und mich dann im Detail überarbeiten, sobald ich das finde tun müssen.

In solchen Fällen habe ich häufig ein generisches IRepository mit dem vollständigen CRUD-Stack erstellt, mit dem ich schnell mit der API spielen und Leute mit der Benutzeroberfläche spielen lassen und parallel Integrations- und Benutzerakzeptanztests durchführen kann. Wenn ich dann feststelle, dass ich bestimmte Fragen zum Repo usw. benötige, beginne ich, diese Abhängigkeit mit der spezifischen Abhängigkeit zu ersetzen, falls erforderlich, und gehe von dort aus. Ein zugrundeliegender 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 über die Schnittstelle mischen und abgleichen und dann über Implementierungen verfügen, die sich um einige oder alle kümmern noch immer die alles machende Implementierung einschleusen, während ich die App erstelle.

paul

26
Paul

Ich bevorzuge bestimmte Repositorys, die vom generischen Repository (oder einer Liste von generischen Repositorys, um das genaue Verhalten anzugeben) mit überschreibbaren Methodensignaturen abgeleitet sind.

12
Arnis Lapsa

Haben Sie ein generisches Repository, das von einem bestimmten Repository umschlossen ist. 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.

5
Peter

öffentliche Klasse UserRepository: Repository, IUserRepository

Sollten Sie IUserRepository nicht injizieren, um zu vermeiden, dass die Schnittstelle verfügbar gemacht wird? Wie bereits erwähnt, benötigen Sie möglicherweise nicht den vollständigen CRUD-Stack usw.

1
Ste