23 Stimmen

Repository Pattern Best Practice

Ich bin also dabei, das Repository-Muster in einer Anwendung zu implementieren und bin dabei auf zwei "Probleme" gestoßen, die mein Verständnis des Musters beeinträchtigen:

  1. Abfragen - Ich habe Antworten gelesen, dass IQueryable nicht verwendet werden sollte, wenn Repositories verwendet werden. Allerdings ist es offensichtlich, dass Sie wollen, so dass Sie nicht eine vollständige Liste von Objekten jedes Mal, wenn Sie eine Methode aufrufen zurückgeben. Sollte es implementiert werden? Wenn ich eine IEnumerable-Methode namens List habe, was ist dann die allgemeine "Best Practice" für eine IQueryable? Welche Parameter sollte sie haben bzw. nicht haben?

  2. Skalare Werte - Wie kann man am besten (unter Verwendung des Repository-Musters) einen einzelnen skalaren Wert zurückgeben, ohne den gesamten Datensatz zurückgeben zu müssen? Wäre es unter dem Gesichtspunkt der Leistung nicht effizienter, nur einen einzigen skalaren Wert über eine ganze Zeile zurückzugeben?

35voto

pfries Punkte 1600

Streng genommen bietet ein Repository eine Sammlungssemantik zum Abrufen/Einfügen von Domänenobjekten. Es bietet eine Abstraktion um Ihre Materialisierungsimplementierung (ORM, hand-rolled, mock), so dass die Konsumenten der Domänenobjekte von diesen Details entkoppelt sind. In der Praxis abstrahiert ein Repository in der Regel den Zugriff auf Entitäten, d.h. auf Domänenobjekte mit Identität und in der Regel einem persistenten Lebenszyklus (in der DDD-Variante bietet ein Repository Zugriff auf Aggregatwurzeln).

Eine minimale Schnittstelle für ein Repository sieht wie folgt aus:

void Add(T entity);
void Remove(T entity);
T GetById(object id);
IEnumerable<T> Find(Specification spec);

Obwohl Sie Unterschiede in der Namensgebung und die Hinzufügung von Save/SaveOrUpdate-Semantik sehen werden, ist die obige Darstellung die "reine" Idee. Sie erhalten die ICollection Add/Remove Mitglieder plus einige Finder. Wenn Sie IQueryable nicht verwenden, werden Sie auch Finder-Methoden auf dem Repository wie sehen:

FindCustomersHavingOrders();
FindCustomersHavingPremiumStatus();

Bei der Verwendung von IQueryable in diesem Zusammenhang gibt es zwei Probleme. Das erste ist die Möglichkeit, dass Implementierungsdetails in Form der Beziehungen des Domänenobjekts an den Client weitergegeben werden, d. h. Verstöße gegen das Gesetz von Demeter. Das zweite Problem besteht darin, dass das Repository Zuständigkeiten für das Auffinden von Daten erhält, die nicht zum eigentlichen Repository des Domänenobjekts gehören, z. B. das Auffinden von Projektionen, die sich weniger auf das angeforderte Domänenobjekt als auf die zugehörigen Daten beziehen.

Außerdem wird durch die Verwendung von IQueryable das Muster "gebrochen": Ein Repository mit IQueryable kann Zugang zu "Domänenobjekten" bieten, muss es aber nicht. IQueryable gibt dem Client eine Menge Optionen darüber, was bei der Ausführung der Abfrage materialisiert werden soll. Dies ist der Hauptpunkt der Debatte über die Verwendung von IQueryable.

Was skalare Werte betrifft, so sollten Sie kein Repository verwenden, um skalare Werte zurückzugeben. Wenn Sie einen Skalarwert benötigen, würden Sie diesen normalerweise von der Entität selbst erhalten. Wenn sich das ineffizient anhört, ist es das auch, aber je nach Ihren Lastmerkmalen/Anforderungen merken Sie das vielleicht nicht. In Fällen, in denen Sie aus Leistungsgründen oder weil Sie Daten aus vielen Domänenobjekten zusammenführen müssen, alternative Sichten auf ein Domänenobjekt benötigen, haben Sie zwei Möglichkeiten.

1) Verwenden Sie das Repository der Entität, um die angegebenen Entitäten zu finden und in eine reduzierte Ansicht zu projizieren/zu mappen.

2) Erstellen Sie eine Finder-Schnittstelle für die Rückgabe eines neuen Domänentyps, der die benötigte reduzierte Ansicht kapselt. Dies wäre kein Repository, da es keine Sammlungssemantik gäbe, aber es könnte bestehende Repositories unter der Haube verwenden.

Wenn Sie ein "reines" Repository verwenden, um auf persistierte Entitäten zuzugreifen, sollten Sie bedenken, dass Sie einige der Vorteile eines ORM beeinträchtigen. In einer "reinen" Implementierung kann der Client keinen Kontext für die Verwendung des Domänenobjekts bereitstellen, so dass Sie dem Repository nicht sagen können: "Hey, ich werde nur die Eigenschaft customer.Name ändern, also mach dir nicht die Mühe, diese Eager-Load-Referenzen zu holen. Auf der anderen Seite stellt sich die Frage, ob ein Kunde von diesen Dingen wissen sollte. Das ist ein zweischneidiges Schwert.

Was die Verwendung von IQueryable anbelangt, so scheinen die meisten Leute damit einverstanden zu sein, das Muster zu "brechen", um die Vorteile der dynamischen Abfragekomposition zu nutzen, insbesondere für Client-Aufgaben wie Paging/Sortierung. In diesem Fall könnten Sie haben:

Add(T entity);
Remove(T entity);
T GetById(object id);
IQueryable<T> Find();

und Sie können dann all diese benutzerdefinierten Finder-Methoden überflüssig machen, die das Repository bei wachsenden Abfrageanforderungen wirklich überladen.

10voto

Stephanvs Punkte 723

Um @lordinateur zu antworten: Ich mag die defacto-Methode zur Angabe einer Repository-Schnittstelle nicht wirklich.

Da die Schnittstelle in Ihrer Lösung erfordert, dass jede Repository-Implementierung mindestens eine Add-, Remove-, GetById- usw. Funktion benötigt. Betrachten wir nun ein Szenario, in dem es keinen Sinn macht, über eine bestimmte Instanz eines Repositorys zu speichern, müssen Sie die verbleibenden Methoden trotzdem mit NotImplementedException oder etwas Ähnlichem implementieren.

Ich ziehe es vor, meine Repository-Schnittstellendeklarationen wie folgt aufzuteilen:

interface ICanAdd<T>
{
    T Add(T entity);
}

interface ICanRemove<T>
{
    bool Remove(T entity);
}

interface ICanGetById<T>
{
    T Get(int id);
}

Eine bestimmte Repository-Implementierung für eine SomeClass-Entität könnte also wie folgt aussehen:

interface ISomeRepository
    : ICanAdd<SomeClass>, 
      ICanRemove<SomeClass>
{
    SomeClass Add(SomeClass entity);
    bool Remove(SomeClass entity);
}

Lassen Sie uns einen Schritt zurücktreten und einen Blick darauf werfen, warum ich dies für eine bessere Praxis halte als die Implementierung aller CRUD-Methoden in einer generischen Schnittstelle.

Einige Objekte haben unterschiedliche Anforderungen als andere. Ein Kunde Objekt darf nicht gelöscht werden, ein PurchaseOrder kann nicht aktualisiert werden, und ein [ ] erstellt werden. Wenn man das generische IRepository-Schnittstelle verwendet, ist dies verursacht dies offensichtlich Probleme bei der Implementierung.

Diejenigen, die das Anti-Muster umsetzen implementieren, werden oft ihre vollständige Schnittstelle und werfen dann Ausnahmen für die Methoden, die sie nicht unterstützen. Abgesehen davon, dass sie nicht einverstanden sind mit OO-Prinzipien verstößt, zerstört dies die Hoffnung auf die Verwendung ihrer IRepository-Abstraktion effektiv nutzen zu können es sei denn, sie fangen auch an, Methoden zu verwenden, um festzustellen, ob bestimmte Objekte unterstützt werden und weiter implementieren sie.

Eine gängige Abhilfe für dieses Problem ist zu granulareren Schnittstellen zu wechseln wie ICanDelete, ICanUpdate, ICanCreate usw. usw. Dies ist zwar umgeht man viele der Probleme die in Bezug auf die OO-Prinzipien aufgetreten sind Prinzipien entstanden sind, reduziert auch die Menge an wiederverwendetem Code, die wiederverwendet wird, da man die meiste Zeit nicht in der Lage ist, das Repository zu verwenden konkrete Instanz nicht mehr verwenden kann.

Keiner von uns schreibt gerne den gleichen Code wieder und wieder zu schreiben. Doch ein Repository Vertrag wie eine architektonische Naht ist der falsche Ort, um den Vertrag zu erweitern, um ihn allgemeiner zu machen.

Diese Auszüge wurden schamhaft entnommen aus diese Stelle wo Sie auch weitere Diskussionen in den Kommentaren lesen können.

4voto

Pete Punkte 12142

Zu 1: Soweit ich es sehen kann, ist es nicht das IQuerable selbst, das das Problem ist, das von einem Repository zurückgegeben wird. Der Sinn eines Repositorys ist, dass es wie ein Objekt aussieht, das alle Ihre Daten enthält. Sie können also das Repository nach den Daten fragen. Wenn Sie mehr als ein Objekt haben, das dieselben Daten benötigt, ist es die Aufgabe des Repositorys, die Daten zwischenzuspeichern, so dass die beiden Clients Ihres Repositorys dieselben Instanzen erhalten - wenn also der eine Client eine Eigenschaft ändert, sieht der andere das, weil sie auf dieselbe Instanz verweisen.

Wenn das Repository tatsächlich der Linq-Provider selbst wäre, dann würde das genau passen. Aber meistens lassen die Leute das IQuerable des Linq-to-sql-Anbieters einfach durchlaufen, wodurch die Verantwortung des Repositorys umgangen wird. Das Repository ist also gar kein Repository, zumindest nach meinem Verständnis und der Verwendung des Musters.

Zu 2: Natürlich ist es leistungsfähiger, nur einen einzelnen Wert aus der Datenbank zurückzugeben als den gesamten Datensatz. Aber mit einem Repository-Muster würden Sie gar keine Datensätze zurückgeben, sondern Geschäftsobjekte. Die Anwendungslogik sollte sich also nicht mit Feldern, sondern mit Domänenobjekten befassen.

Aber inwiefern ist es effektiver, einen einzelnen Wert zurückzugeben, als ein komplettes Domänenobjekt? Sie werden wahrscheinlich nicht in der Lage sein, den Unterschied zu messen, wenn Ihr Datenbankschema einigermaßen gut definiert ist.

Es ist viel wichtiger, sauberen, leicht verständlichen Code zu haben - statt mikroskopisch kleine Leistungsoptimierungen im Vorfeld.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X