W ostatnim wpisie poruszyłem tematykę IoC i w przykładowym kawałku kodu dla LocationRepo znajduje się metoda GetLocations zwracająca typ IQueryable. Zwróciła na to uwagę pewna osoba odwiedzająca bloga i słusznie. Stwierdziłem więc, że lepiej będzie rozpisać się krótko na ten temat niż edytować poprzedni wpis. No więc dlaczego zwracanie typu IQueryable<T> jest złe? Można by powiedzieć, że zwracanie ogólnie jest złe i czasem się zdarza 😉 ale do rzeczy.
Implementując repozytorium powinniśmy tworzyć nową metodę dla każdego zapytania, np. GetLocationsByName, GetLocationsBySth, itd. Nie jest dobrą praktyką utworzenie jednej metody albo właściwości zwracającej całą kolekcję typu IQueryable<T>, po to by następnie filtrować elementy przy pomocy LINQ już poza repozytorium. Zapytania LINQ stosowane dla kolekcji IQueryable<T> są tłumaczone na drzewa wyrażeń, które następnie tłumaczone są na język określonego źródła danych, np. SQL. Zwracając z repozytorium taki typ, odpowiedzialność za tworzenie zapytań do źródła danych przechodzi na dewelopera implementującego kod w warstwie wyższej, warstwie domeny. W konsekwencji kod taki nie jest testowalny w 100%. Nie znamy wszystkich możliwych kombinacji zapytań, które mogą występować.
Kolejnym argumentem, który przemawia za niestosowaniem typu IQueryable w repozytoriach, jest to że uzależniamy nasze repozytorium od konieczności używania Entity Frameworka albo LINQ to SQL, ponieważ w przypadku innych źródeł danych musiałby być zaimplementowany mechanizm zwracania IQueryable. Chcemy aby nasz kod był uniwersalny i nie chcemy utrudniać sobie życia implementując dla każdego nowego źródła danych IQueryable, więc jedynym wyjściem jest unikanie tego typu. Oczywiście czasami może się on przydać, jednak musimy mieć to wszystko na uwadze 😉
Poniżej przykład metod zawartych w repozytorium.
Tak nie robimy
1 2 3 4 |
public IQueryable<Location> GetLocations() { return _ctx.Locations.AsNoTracking(); } |
lepiej zróbmy to tak
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public IEnumerable<Location> GetLocations(int page, int itemsPerPage) { return _ctx.Locations.AsNoTracking() .OrderBy(l => l.Name) .Skip(itemsPerPage*(page - 1)) .Take(itemsPerPage) .ToList(); } public Location GetLocationByName(string name) { return _ctx.Locations.SingleOrDefault(l => l.Name == name); } |
Generalnie żeby nie zwracać IQueryable to się zgadzam. Ale nie zgodzę się ze zdaniem „Implementując repozytorium powinniśmy tworzyć nową metodę dla każdego zapytania”. To nie jest wzorzec repozytorium. Zbyt łatwo wpaść w pułapkę z mnóstwem zbyt wyspecjalizowanych metod, bo masz permutację wiele kombinacji możliwych filtrów zapytania i ograniczania załadowanych powiązanych encji (eager loading). I do tego pewnie dojdzie eksplozja klas DTO.
Been there, done that, got a T-shirt…
Polecam kilka linków to rozważenia innych punktów widzenia:
http://thinkinginobjects.com/2012/08/26/dont-use-dao-use-repository/
http://devpytania.pl/questions/6427/repository-pattern-czy-uzywac
http://fabiomaulo.blogspot.com/2009/09/repository-or-dao-repository.html
Miałem tutaj bardziej na myśli żeby nie robić jednej metody zwracającej wszystkie elementy, po to by później filtrować je poza repo. Tak jak wspomniałeś łatwo wpaść w pułapkę i stworzyć zbyt wiele metod, co też nie jest celem. Dzięki za feedback i linki! 😉
Dzięki za wpis, akurat wczoraj robiłem zaczątek swego repozytorium i zwracałem IQueryable w swoich metodach. Z wpisu i dwóch powyższych komentarzy wychodzi wniosek że wyrzucanie poza repozytorium wszystkich danych a później ich filtrowanie jest złe, a z drugiej strony filtrowanie w repozytorium i wyrzucanie poza repozytorium już wyfiltrowanych danych też jest złe. Więc „jak żyć?” – bo do reszty zgłupiałem.
W tych linka, które wrzuciłem powyżej (i wielu innych, które można znaleźć) powtarza się idea, żeby wyabstrachować jakiś argument wejściowy typu QueryFilter, w którym wołający może przekazać część ograniczeń na kolekcję. Tyle jeżeli chodzi o samo repozytorium, w którym generalnie powinno być tylko kilka metod a la yaml#
IRepository
Get(TId): T
Add(T): void
Query(QueryFilter): IEnumerable
Wszystko poza wykracza poza wzorzec. Wszelkie metody typu GetUserWithOrdersButWithoutCanceledOrdersFilteredByUserEmailAndGender powinny znajdować się gdzieś bliżej „biznesowej” implementacji związanej z konkretnym wymaganiem.
I nie ma nic złego w tym, że takich metod będzie wiele dopóty, dopóki spełniają rolę wymagań biznesu. Problem z wrzuceniem ich wszystkich do repozytorium jest
1. pokusa do ponownego użycia w innej części aplikacji, ponieważ repozytorium jest elementem współdzielonym – potencjał na losowe awarie, gdy będzie potrzeba zmian
2. walka z ORM, jeżeli takowy stoi za repozytorium. Twardy ORM może poważnie ograniczać swobodę wykonywania skomplikowanych zapytań, a często występuje taka trzeba w zołożnych domenach
Czyli wystarczy użyć takiego interfejsu?:
IRepository{
IEnumerable Query(Func query);
void InsertOrUpdate(TEntity entity);
void Delete(TEntity entity)
TEntity GetById(int id)
}
Wydaje się że on ma wszystko co powinno mieć repozytorium. Ewentualnie dodając w argumentach metody Query dane do paginacji.
Jeżeli chcemy mieć funkcje, które zwracają dane wg. jakiś szczególnych filtrów ( GetUserWithOrdersButWithoutCanceledOrdersFilteredByUserEmailAndGender()) tworzymy znowu repo, a może po prostu helpery, (a może skorzystać ze wzorca unit of work?), ale już w warstwie biznesowej (a może w warstwie prezentacji?).
Założę się że odpowiedź na te wszystkie pytania brzmi „zależy” ;]
„Wszelkie metody typu GetUserWithOrdersButWithoutCanceledOrdersFilteredByUserEmailAndGender powinny znajdować się gdzieś bliżej „biznesowej” implementacji związanej z konkretnym wymaganiem.” – czyli gdzie tak naprawdę?
W większości projektów, w których brałem udział, tworzone były specyficzne repozytoria zawierające zestawy metod „pytających” o obiekty. I to miało sens, bo tworzyło jednoznaczny kontrakt, na którym operowała logika biznesowa. W kodzie serwisu biznesowego nie trzeba było się zastanawiać jak coś odpytać, wystarczyło zawołać konkretną metodę z repozytorium. To było SOLIDne i czytelne. I miało dużo większy sens niż popularny antywzorzec w postaci „generycznych repozytoriów”, które potrafią zrobić wszystko z każdym obiektem, ale semantycznie niczego nie wnoszą do kodu.
Ok, to co napisałeś, jest zgodne z definicją z PoEAA Fowlera, ale zrezygnowanie z jednoznacznego kontraktu, który dają specyficzne repozytoria powoduje przecież, że ten wzorzec staje się całkowicie bezużyteczny! Nawet w przypadku DDD. Wychodzi na to, że Fowler wymyślił/opisał antywzorzec.
@Marcin: Extension method mają sens, jak rozumiem poprzez opakowanie tego parametru metody Query.
@dwdkls: nie jestem pewien jak jest z tymi generycznymi repozytoriami jako antywzorzec. repozytorium nie jest abstrakcją na bazę danych, lecz abstrakcją nad zbiór encji biznesowych, które tak się składa, najczęściej siedzą z bazie właśnie. Dodam, że skoro wspomniałeś DDD, byłaby to nie encja lecz Aggregate Root (AR). A AR, jak wiemy, może składać się z kilku encji.
Myśląc innymi kategoriami, repozytorium powinno operować na jednym typie Aggregate Root – stąd typy generyczne wydają się naturalnym wyborem. W moim rozumieniu w repozytorium nie ma miejsca na metod zwracające różnego rodzaju DTO lub operujące na wielu AR. Wtedy mamy do czynienia ze zwykłym DAO, a potrzeba jego utworzenia z reguły wynika bezpośrednio z wymagania biznesowego.
Stąd moje zdanie, o które pytasz „czyli gdzie tak naprawdę?”. Osobiście dla konkretnego widoku/Use Case poszedłbym nawet tak daleko jak całkiem osobne assembly z bezpośrednim dostępem do odpowiedniego źródła danych. Tutaj kłania się też podejście CQRS, w którym w naturalny sposób każdy taki feature może mieć osoby Read Model.
„repozytorium nie jest abstrakcją na bazę danych, lecz abstrakcją nad zbiór encji biznesowych” – no właśnie o to mi chodzi. Skoro repozytorium jest źródłem obiektów biznesowych, to nie ma już nic bliżej biznesu, więc to jest miejsce na metody typu GetUserWithOrdersButWithoutCanceledOrdersFilteredByUserEmailAndGender. To, czy repozytorium wewnętrznie korzysta z ORM, TDG czy DAO nie ma znaczenia. Repozytorium ma operować na AR i zwracać dane dla operacji biznesowych, nie widoku – nigdy nie twierdziłem inaczej. Do wypełniania gridów wypada użyć innego tworu niż repozytorium (albo mojego frameworka ;)) – inna rzecz, że zazwyczaj stosowane są repozytoria.
Dla konkretnych przypadków potrzebne są różne zbiory AR, czyli inne warunki wyszukiwania będziemy mieli np. dla klientów, którzy zrobili zakupy na 1mln zł w poprzednim kwartale, a inne dla tych, którzy mają 5 nieopłaconych faktur. Dodanie metod „ZnajdźKlientówDającychNamMiliony(Money kwota, int liczbaDni)” i „ZnajdźNiesolidnychKlientów(int liczbaFaktur)” do repozytorium wydaje mi się sensownym rozwiązaniem. Serwis biznesowy będzie miał gotowe metody do pobrania danych, i będzie mógł się skupić na tym, co ma z tymi danymi zrobić, a nie na ich pobieraniu – wszystko zgodnie z SRP.
To jest odwieczny problem w programowaniu, wszystko jest zle 🙂
Kazdy mowi cos innego i kazdy ma po troche racje, trzeba nauczyc sie to filtrowac i probowac samemu.
Kwestia doswiadczenia, prob i bledow.
Po części się zgadzam, bo nie ma jednego rozwiązania na wszystkie problemy. Łatwo jest wpaść w taką pułapkę i stąd się biorą te wszystkie „silver bullet”. Chociaż zamiast mówić, że wszystko jest źle, mówiłbym, że nic nie jest w 100% dobre.
Natomiast jest też druga strona medalu. Czy nie uważasz, że jako szeroko pojęta branża IT kręcimy się wokół własnego ogona? Na wiele pytań mamy już odpowiedzi, ale ciągle je zadajemy i ciągle rozwiązujemy pewne problemy od nowa 🙂
Ja bym powiedział raczej, że ciągle wymyślamy te same problemy na nowo. 🙂
Repozytoria są dobre, kiedy są ukierunkowane na warstwę biznesową, a nie bazodanową. Wtedy nawet mając tą samą encję Produkty, mamy różne repozytoria na każdą domenę biznesową i metody modyfikujące tą encję nie są współdzielone pomiędzy domeny. W przeciwnym wypadku tworzymy God Object, który jest używany przez wszystkie domeny i jest ogromny burder. Pisałem o tym na moim blogu http://radblog.pl/pl/2016/01/31/wzorzec-repository-kilka-slow-przeciwko/ i tutajteż jest o tym kilka słów http://piotrgankiewicz.com/2016/03/05/repository-so-we-meet-again/
Zamieniamy słowo „domena” na „bounded context” i zgadzam się w 100% 🙂
Co uzyskujemy gratis, gdy każdy bounded context jest oddzielnym „modułem” (czyli projektem bądź zestawem projektów) w aplikacji.
Normally I don’t learn article on blogs, however I would like
to say that this write-up very compelled me to try and do it!
Your writing style has been surprised me.
Thank you, very nice post.