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 rozmówcy 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: