Simple Command

10 stycznia 2022
software architecture
ruby

Kto kiedykolwiek pracował w projekcie railsowym napewno widział tzw. fat controllers, gdzie logika poszczególnych akcji wpakowana jest bezpośrednio w kontrolery, które automatycznie mają setki metod prywatnych i tysiące linii kodu, a akcje wyglądają np. tak:

Mam taką dobrą praktykę, że przenoszę wszelką logikę z kontrolerów do zewnętrznych domenowych klas serwisów, komend, use case’ów – zwał jak zwał. Testując takie kontrolery, obchodzi mnie tylko kształt odpowiedzi, które zwracają poszczególne akcje oraz to, czy wywoływany został odpowiedni serwis z odpowiednimi parametrami. To wszystko.

Aby zastosować takie rozwiązanie wystarczą PORO (Plain Old Ruby Object), natomiast ja lubię stosować małą, przyjemną bibliotekę Simple Command, która rozszerza nam nasze PORO o kilka rzeczy poprawiających wygodę i testowanie takich klas.

Link do libki: https://github.com/nebulab/simple_command

Z dokumentacji gema możemy wyczytać, że:

  • aby wykorzystać SimpleCommand w swojej klasie należy (tu mam trochę problem z odpowiednim przetłumaczeniem tego na polski), poprzedzić ją właśnie klasą SimpleCommand
  • Jeśli chcesz wiedzieć, o co chodzi z prepend zajrzyj tutaj.

  • będziemy mogli wywołać naszą klasę poprzez zdefiniowaną w niej metodę .call
  • nasza klasa będzie miała dwa atrybuty mówiące o powodzeniu operacji: success? i failure?
  • jeśli operacja się powiedzie, to wartość zwracana przez metodę call będzie dostępna pod atrybutem result
  • jeśli operacja się nie powiedzie, to błędy z tej operacji możemy odczytać z atrybutu errors
  • Testowanie takiej klasy jest bardzo proste. Testujemy tak naprawdę tylko 3 przypadki:

  • czy przy podaniu poprawnych parametrów wywołanie zakończy się sukcesem
  • czy przy podaniu niepoprawnych parametrów otrzymamy błąd
  • czy przy pozytywnym zakończeniu operacji komenda zwróci oczekiwany rezultat
  • Oczywiście, możemy iść głębiej i sprawdzać, czy wewnątrz komendy zostały wywołane jakieś operacje na innych klasach albo czy tablica errors zawiera informacje o konkretnych błędach lub też, czy komenda rzuca wyjątki odpowiednie dla naszych przypadków testowych. Natomiast te trzy powyższe punkty wystarczają, by czuć się komfortowo z implementacją logiki zawartej w komendzie.

    Przejdźmy teraz do konkretnego przypadku logowania użytkownika do API. Kod, który przygotowałem jest framework agnostic, to pokazuje, że SimpleCommand i wzorzec komendy, który wspiera ta biblioteka, można zastosować także poza Ruby on Rails, w skryptach, aplikacjach konsolowych, automatyzacjach itd.

    Klasyczny przykład wg. rails waya:

    Metoda login jest dość długa. Jest parę ifów, jakieś złożone warunki, kilka renderów. Co, jeśli do aplikacji dodamy np. 2 Factor Authentication? Trzeba będzie tę logikę zawrzeć gdzieś między tymi ifami. Wydzielanie tego kodu do metod prywatnych nie do końca zda egzamin. Co, jeśli inna akcja będzie korzystać z tej samej metody, a w międzyczasie zmodyfikujemy metodę do nowych wymagań?

    Przenieśmy całą logikę do komendy LoginUser. Na potrzeby przykładu klasy User i AuthToken są zaślepkami, ich implementacja nas nie interesuje.

    Co tu mamy? Komenda przyjmuje parametry potrzebne do zalogowania. W trakcie egzekwowania logiki rzuca własne wyjątki, łapie te wyjątki i ustawia adekwatnie do nich kody i odpowiedzi błędów. Jest kilka metod prywatnych, wydzielonych dla większej przejrzystości kodu.

    Teraz zobaczmy na test takiej komendy:

    Testy są bardzo proste. Najpierw sprawdzamy happy path, a potem to, jak komenda zachowuje się w przypadku niepoprawnych danych logowania. Tylko tyle i aż tyle.

    A tak będzie wyglądało wywołanie komendy w kontrolerze i test dla akcji logowania:

    A tutaj test:

    Prosty scenariusz. Na poziomie kontrolera sprawdzamy tylko poprawność wywołania komendy z odpowiednimi parametrami. A odpowiedzialność za test logiki i jej poprawność jest wydelegowana do innej warstwy.

    No, ale co teraz? W momencie, kiedy będziemy mieć więcej takich akcji, które korzystają z komend, czeka nas niezła ifologia. Możemy sobie to ogarnąć przez pomocnicze metody. Na potrzeby przykładu wrzuciłem je do includowanego modułu CommandHelpers. Komenda jest przechwytywana przez handle i on już odpowiada za prawidłowe obsłużenie jej odpowiedzi lub błędów. Zawsze ten handler można bardziej rozbudować i dostosować. Przedstawiam Ci natomiast minimalną wersję implementacji.

    Potem w kontrolerze wystarczy coś takiego:

    I już 😀

    W efekcie mamy enkapsulację logiki w przyjemnej klasie zawierającej dodatkowo informacje o powodzeniu lub przerwaniu akcji oraz czyste i przejrzyste kontrolery.

    Uwaga! W repozytorium SimpleCommand na Githubie może Ci się rzucić w oczy, że nie ma tam żadnych nowych commitów od kilku lat. Biblioteka wygląda na nieutrzymywaną, a więc możesz mieć obawy przed jej użyciem. Nie martw się. Wystarczy zajrzeć w kod gema i zobaczyć, że cały SimpleCommand to tak naprawdę czyste PORO bez żadnych zależności. Równie dobrze zamiast zainstalowania gemu możemy sobie ten kod zawrzeć w jakiejś pomocniczej klasie w aplikacji railsowej.