Optymalizacja kodu za pomocą Query Objects

Niejednokrotnie w swojej pracy spotkaliście się zapewne z przeładowanymi modelami i ogromną ilością wywołań w kontrolerach. W tym artykule chciałbym zaprezentować proste rozwiązanie bazujące na znanej już wiedzy w środowisku rails.


Tomasz Szkaradek. Expert Ruby Developer oraz Development Manager w Codest. Zawodowo z programowaniem związany od 8 lat. Szkoli i dzieli się wiedzą z innymi. W latach 2017-2018 pełnił rolę mentora w szkole programowania Code Sensei.


Bardzo ważnym aspektem w aplikacji raislowej jest minimalizacja ilości nadmiarowych zależności, dlatego też całe środowisko rails w ostatnim czasie promuje podejście z service object i użycie metody PORO, czyli Pure Old Ruby Object. Opis jak użyć takiego rozwiązania w swoim projekcie możecie przeczytać tutaj. Natomiast w tym artykule rozwinę  ten koncept i dostosuję do poruszanego problemu.

Problem

W hipotetycznej aplikacji mamy skomplikowany system transakcji. Model reprezentujący każdą transakcję posiada zestaw scopów, które pomagają pobierać dane. Jest to wspaniałe ułatwienie pracy, ponieważ wszystko znajduje się w jednym miejscu. Jednak do czasu. Niestety, wraz z rozwojem aplikacji rozwija się skomplikowanie projektu. Scopy już nie mają prostych odwołań where, brakuje nam danych, zaczynamy wczytywać relacje. Po jakimś czasie przypomina to niestety skomplikowany system luster. A co gorsza, nie wiemy jak należy robić wielolinijkowe lambdy!

Poniżej znajduje się rozbudowany już model aplikacji. Przechowywane są w nim transakcje systemu płatniczego. Jak widać, już w tym momencie zaczyna być problematyczny.

Model to jedno, natomiast wraz ze zwiększającą się skalą naszego projektu, kontrolery również zaczynają “puchnąć”. Popatrzmy na przykład poniżej:

Widzimy tutaj wiele linijek chainowanych metod, są też dodatkowe joiny, których nie chcemy przeprowadzać w wielu miejscach, tylko w tym konkretnym. Załączone dane są później wykorzystywane przez metodą apply_filters, która dorzuca odpowiednie zawężanie danych na podstawie parametrów GET. Oczywiście część z tych odwołań możemy jak najbardziej przenieść do scopa, ale czy to nie jest właśnie problem, który próbujemy rozwiązać?

Rozwiązanie

Skoro już wiemy, że mamy problem, musimy przystąpić do działania. Bazując na zamieszczonym we wstępie artykułu odnośniku, zastosujemy tutaj podejście z PORO. W tym wypadku takie podejście nazywa się query object. Jest to rozwinięcie konceptu service objects.

Utwórzmy nowy katalog o nazwie services znajdujący się w katalogu apps naszego projektu. W nim utworzymy klasę o nazwie TransactionsQuery.

W następnym kroku należy utworzyć initializer, w którym będziemy tworzyć domyślną ścieżkę wywołania dla naszego obiektu.

Dzięki temu mamy możliwość przeniesienia relacji z active record do naszego obiektu. Teraz spokojnie jesteśmy w stanie przenieść wszystkie nasze scopy do klasy, które są potrzebne tylko w przedstawionym kontrolerze.

Brakuje jeszcze najważniejszej części, czyli zebrania danych w jeden ciąg i upublicznienie interfejsu. Metodą, w której wszystko skleimy nazwiemy call. Co ważne, w niej skorzystamy ze zmiennej instancyjnej @scope, gdzie znajduje się scope naszego wywołania.

Cała klasa prezentuje się w następujący sposób:

Model po wszystkich porządkach wygląda zdecydowanie lżej. W nim skupiamy się tylko i wyłącznie na walidacji danych i relacjach pomiędzy innymi modelami.

W kontrolerze udało się już wdrożyć nasze rozwiązanie, przeniesione zostały wszystkie dodatkowe zapytania do osobnej klasy. Natomiast wciąż nierozwiązaną kwestią pozostają wywołania, które nie znajdowały się w modelu. Akcja index po zmianach wygląda w następujący sposób:

Rozwiązanie

W przypadku wdrażania dobrych praktyk i konwencji, optymalnym rozwiązaniem jest wymiana wszystkich podobnych wystąpień danego problemu, dlatego też w tym wypadku przeniesione zostaną zapytania sql z akcji index do osobnego query object. Taką klasę nazwiemy TransactionsFilterableQuery. Styl, w jakim przygotujemy klasę będzie zbliżony do tego, który był zaprezentowany w TransactionsQuery. W ramach zmian w kodzie przemycony zostanie bardziej intuicyjny zapis dużych zapytań sql, za pomocą wielolinijkowych łańcuchów znaków zwanych heredoc. Rozwiązanie dostępne jest poniżej:

W przypadku zmian w kontrolerze redukujemy dużą ilość linijek, dzięki dodaniu queryobject. Ważne jest, aby wydzielić wszystko oprócz części odpowiedzialnej za paginację.

Podsumowanie

Query Object wiele zmieniają w podejściu do pisania zapytań sql. W ActiveRecord bardzo łatwo jest umieszczać całą logikę biznesową i bazodanową w modelu. Wszystko znajduje się w jednym miejscu, natomiast to jest dobre dla mniejszych aplikacji. Wraz ze wzrostem skomplikowania projektu wydzielamy logikę do innych miejsc. Same query object pozwalają grupować zbiory zapytań składowych w konkretnym problemie. Dzięki czemu mamy łatwą możliwość późniejszego dziedziczenia kodu i dzięki duck typing można również stosować te rozwiązania w innych modelach.

Wadą takiego rozwiązania jest większa ilość kodu i rozdrobnienie odpowiedzialności, natomiast to czy chcemy podjąć takie wyzwanie zależy od nas i tego, jak bardzo przeszkadzają nam fat models.


Artykuł został pierwotnie opublikowany na codesthq.com/blog/. Zdjęcie główne artykułu pochodzi z kaboompics.com.

Patronujemy

 
 
Polecamy
Escape from Merge Hell: Why I Prefer Trunk-Based Development Over Feature Branching and GitFlow