Wzorzec Repository – kilka słów przeciwko

Początki nowego projektu zawsze są interesujące – można posprzeczać się na tematy możliwych do użycia technologii / wzorców / planowanej architektury. Później, gdy już projekt zastyga i klepiemy tylko kolejne widoki każda kolejna próba takiej dyskusji kończy się tekstem typu: „Ale po co o tym gadać – i tak nic nie zmienimy bo trzeba by całą aplikację przepisywać”.

I tak sobie rozmawiając trafiliśmy na wzorzec Repository. Ja objawiłem się tutaj jako duży przeciwnik tego wzorca, moi interlokutorzy zaś byli za nim. Prowadząc dyskusję wyklarowałem kilka ciekawych argumentów którymi chciałbym się tutaj podzielić. Ta dyskusja była w internecie przeprowadzana już tysiąc razy, ale wyszło że trzeba ją przeprowadzić jeszcze raz.

Dla mnie wzorzec Repository, w obecnym kształcie w jakim jest on stosowany czyli nakładka na Entity Framework / NHibernate, jest indoktrynacją jaką wtłaczały w w nas mądre głowy, szczególnie z Microsoftu, stosując od lat te same wzorce w infrastrukturach które tych repozytoriów nie potrzebują. Repozytoria powstały w czasach kiedy stosowano je by ukryć zapytania SQLowe przed logiką biznesową. Aktualnie zaś sam EF’owy DbContext implementuje interfejs Repository, przez co tworzymy abstrakcję nad abstrakcją bazy danych:

A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that it can be used to query from a database and group together changes that will then be written back to the store as a unit. – MSDN

Repozytoria promowane w obecnym kształcie mają swoje wady:

  • Promują duże klasy które są agregatorami wszystkich metod nad daną encją. Nie jest ważne biznesowe użycie danej metody, mamy zbiór niepowiązanych ze sobą metod mających tylko wspólne źródło danych. Tworzy się później śmietnisko jednorazowych metod.
  • Metody łączące ze sobą 2 lub więcej encji są umieszczane wg subiektywnej opinii w jednym z repozytoriów. W następstwie osoba uważająca inaczej albo straci czas szukając tej metody, lub stworzy jej odpowiednik w drugim repozytorium.
  • Tworzymy opakowania na mechanizmy bazy danych, które uniemożliwiają nam skorzystanie z nich bez dodawania kolejnych metod. Lazy loading/eager fetching, cache 1/2 poziomu, transakcje i wiele innych.
  • Repozytoria posiadające jednocześnie metody zapisu i odczytu łamią zasadę SRP – Bogard dobrze pokazał to na przykładzie dekompozycji wzorca Repository w swojej prezentacji.
  • Repozytoria zachęcają do wprowadzania wyłomów tworząc metody zwracające IQueryable, metody pozwalające filtrować z przekazaniem funkcji filtrującej itd. Wszystko to by szybciej pisać i omijać problemy z dodawaniem kolejnych metod do repozytorium. Jednak tworzy to problem przenoszenia logiki wyciągania danych z bazy do użytkowników repozytorium.

Dużo osób wzbrania się przez użyciem bezpośrednio w kodzie kontekstu bazy przez opinię, że wtedy nie można poprawnie testować kodu ponieważ musimy zapewnić bazie dane na jakich ma kontekst operować. I wg mnie jest to niesłuszna obawa. Można sprawić by kontekt używał pod spodem listy encji / bazy in-memory, dzięki czemu testy będą przebiegały szybciej, a my będziemy mieli lepiej przetestowany kod. Robienie abstrakcji sprawi że napiszemy więcej kodu, a będziemy mieli mniejszą pewność że kod działa tak jak powinien.

Ja jako zamienniki repozytorium używałbym (w zależności od skomplikowania domeny):

  • W prostych domenach (słowniki, listy) używanie wprost DbContextu lub prostego interfejsu na DbContext’cie który zwraca IDbSet danego typu. Mamy przez to możliwość mockowania i zachowaną prostotę.
  • Można również wstrzykiwać bezpośrednio IDbSet i wtedy mamy jedynie możliwość robienia zapytań. Dla prostych przypadków np. zapytania po obiekt przez identyfikator to bardzo słuszna droga.
  • DataSourceResult od kendo które potrafi zwrócić dane z grida odpowiednio przefiltrowane / posortowane itd.
  • Query Objecty które tworzymy dla bardziej skomplikowanego żądania danych. Dzieki temu mamy większą granulację i zachowaną zasadę SRP.
  • AutoMapper i ProjectTo, który zamienia mapowanie encji -> dto na kod SQL wykonywany w jednym zapytaniu.
  • Breeze który pozwala wystawić interfejs IQueryable na zewnątrz i odpowiednio queryować zbiór danych. Są dodatki do prawie każdego języka i źródła danych.

Szczególnie ciekawa jest idea query objectów (finderów) – pisali o tym Bogard i Ayende. Tworzymy obiekt zapytania, która wystawia metodę Execute (tak jest u Bogarda) zwracającą dane odpowiednio przefiltrowane i zmapowane. Jest to świetny sposób by sprawić by nasz kod był bardziej zorientowany domenowo, nie łamał zasady SRP i był całkowicie testowalny. Używałem tego wzorca podczas pisania jednej z mojej aplikacji i kod był o wiele lepiej zrozumiały i rozszerzalny niż w przypadku użycia wzorca Repository. Każda domena aplikacji miała własne query, które mapowały dane z bazy do odpowiedników biznesowych używanych w danych domenach.

Wzorzec Repository ma sens jeśli będzie ukrywał pod sobą dodatkową logikę, która rozszerzy działanie ORM o niedostępną dla niego funkcjonalność. Taką logiką może być np. warstwa bezpieczeństwa, która do każdego zapytania doda warunki sprawdzające czy użytkownik ma prawo dostępu do wyszukiwanych danych. Ale w 95% przypadków tworzymy jedynie niepotrzebną przeplotkę nad ORM, która sprawia że tracimy czas i mamy coraz większe spaghetti w kodzie. Chcąc ułatwić sobie życie tworzymy tylko więcej problemów. Wzorzec Repository jest nienaturalnie nadużywany, co zauważyli już sami propagatorzy DDD. Ayende pisze w swoim artykule: Quite frankly, and here I fully share the blame, the Repository pattern is popular. Dlatego polecam go używać jedynie w przypadkach gdy widzimy realną potrzebę jego wykorzystania, a nie stoi za nami duch przeszłości, mówiący że skoro tak ludzie robili od zawsze to teraz też tak zróbmy. Wcale nie.

Na koniec zostawię zagregowaną listę artykułów / postów mających podobne uwagi w tym temacie. Na nich się wzorowałem pisząc ten artykuł i są one rozszerzeniem tego posta:

  • Pingback: dotnetomaniak.pl()

  • http://www.kamiljozwiak.net Kamil Jóźwiak

    Pytanie czy zapis i odczyt w repo narusza SRP? pewnie tak ale czy stosowanie DbContext nie narusza DIP? Ja jednak będę bronił wzorca bo wg. mnie łatwiej jest zmokować repo niż je nastrzykiwać.
    Pytanie jak zastąpić brak repo w przykadku architektury ports and adapters?

    • Radosław Maziarka

      Nie za bardzo rozumiem jak DbContext ma naruszać DIP – wstrzykuje się go, czy same IDbSety identycznie jak repozytoria.

      Odnośnie mocków / wstrzykiwania to preferowałbym testy na danych, głównie in-memory, lub do cięższych rzeczy zastosowanie Query Objectów.

      W architekturze heksagonalnej (wolę to określenie niż ports and adapters) mamy w centrum Application Domain. I tam zamiast Repository używamy jednego z wzorców opisanych powyżej.

    • http://blog.sasin.eu Assassyn

      Zawsze mozesz uzyc CQRS (ktora wspiera SRP) i takze bardzo latwo to zastapic mockiem albo stubem.

      No i nie ma problemow w P&A. U mnie przewaznie jeden modul wystawia interface do dostepu do danyc, a inny modul implmentuje dostep do bazy danych. Takie rozwiazanie pozwala takze na szybka podmiane bazy danych jesli potrzeba.

      • Radosław Maziarka

        Czym jest P&A? :)

        • http://www.kamiljozwiak.net Kamil Jóźwiak

          Ports and Adapters :)

  • http://blog.sasin.eu Assassyn

    Czy mi sie wydaje czy „idea query objectów (finderów)” to po prostu CQ(R)S?

  • http://moromind.pl/ Grzegorz Morawski

    Fajny post. Dobrze podsumowuje bolączki związane z repository. Takie „sprzeczki” w nowym projekcie są bardzo istotne i warto je kultywować. Najsłabszym podejściem jest zebranie kilku wzorców, ponieważ „w poprzednim projekcie działało”, albo „tak się robi od lat”. To, że wzorzec/technologia X był dobry do projektu Y nie oznacza, że warto go użyć w projekcie Z. Więcej! To że był dobry w projekcie Y nie musi oznaczać, że nadal byłby dobry gdybyśmy teraz zaczynali pisać projekt Y. Nie zawsze jesteśmy tego świadomi.

  • Michal Michal

    No dobrze – a co jesli mamy zapytanie, ktore jest wywolywane w roznych serwisach – umieszczenie go w repozytorium umozliwia to, ze to zapytanie nie bedzie zduplikowane w kazdym serwisie, ktory go potrzebuje – a skoro nie uzywasz repozytoriow to gdzie takie zapytania umieszczasz?

    • Radosław Maziarka

      Zależy jakie to jest zapytanie. Na pewno współdzielony query object da radę. Pomyślałbym także nad przemyśleniem czy naprawdę potrzebujemy takiego samego zapytania w kilku miejscach – widziałem wiele razy jak ludzie używali współdzielonych metod z repozytoriów by rezultaty rozwijać jakimiś dodatkowymi helperami. W takim miejscu rozbiłbym to na mniejsze obiekty ściśle zorientowane na daną domenę.

  • Krystian Czaplicki

    Uwagi wartę przemyślenia, sam bardzo często łapie się, że wykorzystuje wiele wzorców bądź znanych mi technologii wyłącznie dlatego, że je znam i wykorzystywałem we wcześniejszych projektach pomijając analizę czy naprawdę tego potrzebuje. Co do samego wzorca to staram się go wykorzystywać z prostego powodu, często występuje potrzeba pewnego filtrowania danych, która pojawia się w połowie projektu. Przykładowo długi czas operowałem na podstawowych rolach użytkowników, lecz później powstaje potrzeba dodania specjalnych mapowań rola – funkcjonalność, tak aby możliwe było dodawanie uprawnień wyłącznie do niektórych akcji rozszerzając tym samym podstawowe uprawnienia i w wypadku, gdybym operował na czystym kontekście to wszystkie zapytania musiałbym odnaleźć oraz odpowiednio rozszerzyć, natomiast w przypadku Repository mogę to zrobić już w samej klasie związanej z wzorcem. Oczywiście nie bez uwagi przechodzi fakt, że używanie tego wzorca znacznie zwiększa czytelność kodu, przynajmniej w moim wykonaniu, lecz Twój tekst dał mi do myślenia i następnym razem dwa razy się zastanowię zanim go zastosuję.

  • Pingback: Hryniewski.NET | Wzorzec Repozytorium i Unit of Work()