Czy istnieje realna zaleta ogólnego repozytorium?

28

Czytałem kilka artykułów na temat zalet tworzenia Ogólnych repozytoriów dla nowej aplikacji ( przykład ). Pomysł wydaje się miły, ponieważ pozwala mi używać tego samego repozytorium do robienia kilku rzeczy dla kilku różnych typów jednostek jednocześnie:

IRepository repo = new EfRepository(); // Would normally pass through IOC into constructor 
var c1 = new Country() { Name = "United States", CountryCode = "US" };
var c2 = new Country() { Name = "Canada", CountryCode = "CA" };
var c3 = new Country() { Name = "Mexico", CountryCode = "MX" };
var p1 = new Province() { Country = c1, Name = "Alabama", Abbreviation = "AL" };
var p2 = new Province() { Country = c1, Name = "Alaska", Abbreviation = "AK" };
var p3 = new Province() { Country = c2, Name = "Alberta", Abbreviation = "AB" };
repo.Add<Country>(c1);
repo.Add<Country>(c2);
repo.Add<Country>(c3);
repo.Add<Province>(p1);
repo.Add<Province>(p2);
repo.Add<Province>(p3);
repo.Save();

Reszta implementacji repozytorium jest jednak w dużym stopniu zależna od Linq:

IQueryable<T> Query();
IList<T> Find(Expression<Func<T,bool>> predicate);
T Get(Expression<Func<T,bool>> predicate);
T First(Expression<Func<T,bool>> predicate);
//... and so on

Ten wzorzec repozytorium działał fantastycznie dla Entity Framework i prawie oferował mapowanie 1 do 1 metod dostępnych w DbContext / DbSet. Ale biorąc pod uwagę powolne wdrażanie Linq w innych technologiach dostępu do danych poza Entity Framework, jaką to daje przewagę nad bezpośrednią pracą z DbContext?

Próbowałem napisać PetaPoco wersję repozytorium, ale PetaPoco nie obsługuje Linq wyrażeń, co sprawia, tworząc ogólną IRepository interfejs prawie bezużyteczne, chyba że tylko używać go do podstawowego GETALL, GetById, dodawać, aktualizować, usuwać i Zapisz metody i wykorzystać jako klasę podstawową. Następnie musisz utworzyć określone repozytoria za pomocą specjalistycznych metod do obsługi wszystkich klauzul „gdzie”, które wcześniej mogłem przekazać jako predykat.

Czy wzorzec Ogólnego repozytorium jest użyteczny dla czegokolwiek poza Entity Framework? Jeśli nie, to dlaczego ktoś miałby go używać zamiast pracować bezpośrednio z Entity Framework?


Oryginalny link nie odzwierciedla wzoru, którego użyłem w moim przykładowym kodzie. Oto ( zaktualizowany link ).

Sam
źródło
1
Czy pytanie dotyczy tak naprawdę „Advatange of Generic Repository”, czy jest bardziej „jak ukryć złożone zapytania za ogólnym interfejsem repozytorium”? Czy można być ogólnym, jeśli jego interfejs i użycie zależy od linq? Nasze Repositoryapi ma metodę QueryByExample, która jest całkowicie niezależna od techniki wyszukiwania i umożliwia zmianę implementacji.
k3b
„Pozwala mi używać tego samego repozytorium do robienia kilku rzeczy dla kilku różnych typów jednostek” => Czy to ja, czy po prostu źle zrozumiałeś, co to jest ogólne repozytorium (w tym w odniesieniu do wspomnianego artykułu)? Dla mnie ogólne repozytorium zawsze oznaczało szablonowanie wszystkich repozytoriów za pomocą jednej klasy lub interfejsu, a nie posiadanie pojedynczej instancji repozytorium zarządzającej trwałością wszystkich encji bez względu na ich typ ...
guillaume31

Odpowiedzi:

35

Ogólne repozytorium jest nawet bezużyteczne (a IMHO również złe) dla Entity Framework. Nie wnosi żadnej dodatkowej wartości do tego, co już zostało dostarczone IDbSet<T>(czyli przez zwykłe repozytorium).

Jak już odkryłeś, argument, że ogólne repozytorium można zastąpić implementacją innych technologii dostępu do danych jest dość słaby, ponieważ może wymagać napisania własnego dostawcy Linq.

Drugi wspólny argument dotyczący uproszczonego testowania jednostek jest również błędny, ponieważ kpiące repozytorium / zestaw z pamięcią danych w pamięci zastępuje dostawcę Linq innym, który ma różne możliwości. Dostawca Linq do podmiotów obsługuje tylko podzbiór funkcji Linq - nawet nie obsługuje wszystkich metod dostępnych w IQueryable<T>interfejsie. Udostępnianie drzew wyrażeń między warstwą dostępu do danych a warstwami logiki biznesowej zapobiega fałszowaniu warstwy dostępu do danych - logikę zapytań należy oddzielić.

Jeśli chcesz mieć silną „ogólną” abstrakcję, musisz również zastosować inne wzorce. W takim przypadku należy użyć abstrakcyjnego języka zapytań, który można przetłumaczyć z repozytorium na określony język zapytań obsługiwany przez używaną warstwę dostępu do danych. Jest to obsługiwane przez wzorzec specyfikacji. Linq on IQueryablejest specyfikacją (ale tłumaczenie wymaga dostawcy - lub jakiegoś niestandardowego użytkownika tłumaczącego drzewo wyrażeń na zapytanie), ale możesz zdefiniować własną uproszczoną wersję i użyć jej. Na przykład NHibernate używa Criteria API. Nadal najprostszym sposobem jest użycie określonego repozytorium za pomocą określonych metod. Ten sposób jest najprostszy do wdrożenia, najprostszy do przetestowania i najprostszy do sfałszowania w testach jednostkowych, ponieważ logika zapytań jest całkowicie ukryta i oddzielona od abstrakcji.

Ladislav Mrnka
źródło
ISessionKrótkie pytanie, NHibernate ma to, co można łatwo wyśmiewać do ogólnych celów testowania jednostkowego, i chętnie opuściłbym „repozytorium” również podczas pracy z EF - ale czy jest jakiś łatwiejszy, prostszy sposób na odtworzenie tego? Coś podobnego do ISessiontego ISessionFactory, bo nie ma, IDbContexto ile mogę powiedzieć ...
Patryk Ćwiek
Nie, nie ma IDbContext- jeśli chcesz IDbContext, możesz po prostu utworzyć taki i zaimplementować go w kontekście pochodnym.
Ladislav Mrnka
Mówisz więc, że dla codziennych aplikacji MVC IDbSet <T> powinien zapewnić wystarczająco dobre ogólne repozytorium, aby całkowicie zapomnieć o wzorcu repozytorium. Z wyjątkiem wyjątkowych sytuacji, np. Gdy nie masz żadnych odniesień do DAL w swoim projekcie MVC.
ProfK
1
Dobra odpowiedź. Niektórzy programiści tworzą po prostu kolejną warstwę abstrakcji bez żadnego powodu. IMO, EF nie potrzebują ogólnego repozytorium ani jednostki pracy (są
wbudowane
1
IDbSet <T> to ogólne repozytorium z zależnością od Entity Framework, które w pewnym sensie nie pozwala na użycie ogólnego repozytorium do wyodrębnienia zależności od Entity Framework.
Joel McBeth,
7

Problemem nie jest wzorzec repozytorium. Dobrze jest mieć abstrakcję między uzyskiwaniem danych a sposobem ich uzyskiwania.

Problemem jest tutaj implementacja. Zakładanie, że do filtrowania zadziała dowolne wyrażenie, jest co najwyżej ryzykowne.

Utworzenie repozytorium dla wszystkich twoich obiektów bezpośrednio mija się z celem. Obiekty danych rzadko, jeśli w ogóle, będą mapowane bezpośrednio na obiekty biznesowe. Przekazywanie T do filtrowania ma w tych sytuacjach znacznie mniej sensu. A zapewnienie tak dużej funkcjonalności gwarantuje, że nie będziesz w stanie obsłużyć wszystkich, gdy pojawi się inny dostawca.

Telastyn
źródło
Dlatego bardziej sensowne jest posiadanie repozytorium na obiekt danych lub grupę ściśle ze sobą powiązanych obiektów, przy użyciu określonych metod, takich jak GetById (int id), SortedList () itp.? Czy zwracam listy obiektów danych z repozytorium, czy też przekształcam je w obiekty biznesowe z niezbędnymi polami? Trochę mi się wydawało, że tak się stało w warstwie serwisowej / biznesowej.
Sam
1
@sam - Zwykle preferuję bardziej szczegółowe repozytoria, tak. To, czy tłumaczą, czy nie, zależy od tego, gdzie rzeczy mogą się zmienić. Jeśli Twoja domena jest dobrze zdefiniowana, zwrócę rzeczy w kształcie domeny. Jeśli dane są dobrze zdefiniowane, zwracam rzeczy w kształcie struktur danych. Jeśli nie są, miałbym byt, który służy jako dobrze zdefiniowana podstawa do zbudowania, a następnie przystosowania się do niego / z niego.
Telastyn
1
Nie ma powodu, dla którego nie można mieć określonych repozytoriów, które delegują wspólne funkcje do ogólnego repozytorium, ale ma także dodatkowe metody specyficzne dla repozytorium dla bardziej złożonych wywołań. Widziałem to już kilka razy.
Eric King,
@EricKing Ja też mam i było tam dobrze, ale ma tendencję do wycieku pewnej abstrakcji, ponieważ wspólne rzeczy zwykle istnieją tylko ze względu na powszechność sposobu przechowywania danych (na przykład GetByID wymaga tabel z identyfikatorami).
Telastyn
@Telastyn Tak, zgodziłbym się z tym, ale dzieje się tak również w przypadku określonych repozytoriów. Nieszczelne abstrakcje nie są specyficzne dla ogólnych repozytoriów. (Wow, czy mógłbym to jeszcze bardziej niezdarnie sformułować?)
Eric King,
2

Wartość ogólnej warstwy danych (repozytorium jest szczególnym typem warstwy danych) pozwala kodowi na zmianę podstawowego mechanizmu pamięci, z niewielkim lub żadnym wpływem na kod wywołujący.

Teoretycznie działa to dobrze. W praktyce, jak zauważyłeś, abstrakcja jest często nieszczelna. Mechanizmy używane do uzyskiwania dostępu do danych w jednym są inne niż mechanizmy w innym. W niektórych przypadkach kończy się to pisaniem kodu dwukrotnie: raz w warstwie biznesowej i powtarzając go w warstwie danych.

Najskuteczniejszym sposobem utworzenia ogólnej warstwy danych jest wcześniejsza znajomość różnych rodzajów źródeł danych, z których będzie korzystać aplikacja. Jak widzieliście, założenie, że LINQ lub SQL jest uniwersalne, może stanowić problem. Próba modernizacji nowych magazynów danych prawdopodobnie spowoduje przepisanie.

[Edycja: Dodano następujące elementy.]

Zależy to również od tego, czego aplikacja potrzebuje od warstwy danych. Jeśli aplikacja ładuje lub przechowuje obiekty, warstwa danych może być bardzo prosta. Gdy rośnie potrzeba wyszukiwania, sortowania i filtrowania, wzrasta złożoność warstwy danych, a abstrakcje stają się nieszczelne (na przykład ujawnianie zapytań LINQ w pytaniu). Jednak gdy użytkownicy mogą dostarczyć własne zapytania, należy dokładnie rozważyć koszty / korzyści warstwy danych.

akton
źródło
1

Posiadanie warstwy kodu nad bazą danych jest warte zachodu w prawie wszystkich przypadkach. Generalnie wolałbym wzorzec „GetByXXXXX” we wspomnianym kodzie - pozwala on optymalizować zapytania za nim w razie potrzeby, jednocześnie utrzymując interfejs wolny od bałaganu interfejsu danych.

Korzystanie z generycznych jest zdecydowanie uczciwą grą - posiadanie Load<T>(int id)metody ma mnóstwo sensu. Ale budowanie repozytoriów wokół LINQ jest równoważne z 2010 r. Porzucaniem zapytań sql wszędzie z niewielkim dodatkowym bezpieczeństwem.

Wyatt Barnett
źródło
0

Cóż, z podanym linkiem widzę, że może to być przydatne opakowanie dla DataServiceContext, ale nie zmniejsza manipulacji kodem ani nie poprawia czytelności. Poza tym dostęp do DataServiceQuery<T>jest utrudniony, co ogranicza elastyczność do .Where()i .Single(). Nie są AddRange()ani alternatywy. Nie Delete(Predicate)podano też, co mogłoby być przydatne ( repo.Delete<Person>( p => p.Name=="Joe" );aby usunąć Joe-ów). Itp.

Wniosek: taki interfejs API blokuje natywny interfejs API i ogranicza go do kilku prostych operacji.

aiodintsov
źródło
Masz rację. To nie był artykuł wykorzystujący wzorzec, który mam w moim przykładowym kodzie. Spróbuję znaleźć link, kiedy wrócę do domu.
Sam
Dodano zaktualizowany link na dole pytania.
Sam