Najczęstsze błędy podczas tworzenia aplikacji webowej w Ruby on Rails

Ruby / Ruby on Rails
Oleksandr Vykhor
41
02-07-2020 16:24:00


Ruby on Rails (lub po prostu „Rails”) to popularny framework open source oparty na języku programowania Ruby, którego celem jest uproszczenie i uporządkowanie procesu tworzenia aplikacji webowych.

Rails opiera się na zasadzie „konwencja ponad konfiguracją”. Mówiąc prościej, oznacza to, że domyślnie Rails zakłada, iż doświadczeni programiści będą stosować „standardowe” dobre praktyki (dotyczące nazewnictwa, struktury kodu itp.), a jeśli tak zrobią, wszystko zadziała „automagicznie”, bez potrzeby doprecyzowywania czegokolwiek. Taki paradygmat ma swoje zalety, ale nie jest wolny od pułapek. W szczególności „magia”, która dzieje się za kulisami frameworka, może czasem prowadzić do bólu głowy, zamieszania i problemów w stylu „co tu się, do licha, dzieje?”. Mogą też pojawić się niepożądane konsekwencje dla bezpieczeństwa i wydajności.

Tak więc, choć Rails jest prosty w użyciu, łatwo też napytać sobie z nim biedy. Ten poradnik omawia 10 najczęstszych problemów, pokazuje, jak ich unikać, i wyjaśnia, co je powoduje.

Błąd 1. Zbyt dużo logiki w kontrolerze

Rails opiera się na architekturze MVC. W społeczności Rails mówimy o grubym modelu i chudym kontrolerze, jednak ostatnio widziałem kilka aplikacji Rails, które łamały tę zasadę. Bardzo łatwo jest przenieść do kontrolera całą logikę widoku (której miejsce jest w helperze) albo logikę modelu.

Problem polega na tym, że obiekt kontrolera zaczyna łamać zasadę pojedynczej odpowiedzialności, przez co przyszłe zmiany w kodzie stają się trudne i podatne na błędy. Ogólnie rzecz biorąc, w kontrolerze powinny znaleźć się tylko następujące rodzaje logiki:

Obsługa sesji i cookies. Może to obejmować także uwierzytelnianie/autoryzację lub dowolne inne operacje na cookies, których potrzebujesz.

Wybór modelu. Logika wyszukiwania właściwego obiektu modelu na podstawie parametrów przekazanych w żądaniu. Najlepiej, aby było to wywołanie pojedynczej metody find, ustawiające zmienną używaną później do wygenerowania odpowiedzi.

Zarządzanie parametrami żądania. Zbieranie parametrów żądania i wywołanie odpowiedniej metody modelu.

Renderowanie/przekierowanie. Renderowanie wyniku (html, xml, json itp.) lub przekierowanie, jeśli jest potrzebne.

Choć nadal nagina to zasadę pojedynczej odpowiedzialności, jest to swego rodzaju minimum, którego framework Rails wymaga od kontrolera.

Błąd 2. Zbyt dużo logiki w widoku

ERB, wbudowany silnik szablonów Rails, daje świetne możliwości tworzenia stron o zmiennej treści. Jeśli jednak nie zachowasz ostrożności, możesz skończyć z dużym plikiem, w którym HTML jest wymieszany z kodem Ruby, trudnym w zarządzaniu i utrzymaniu. Może to również prowadzić do wielu powtórzeń, łamiących zasadę DRY (don’t repeat yourself).

Może się to przejawiać na wiele sposobów. Jednym z nich jest nadmiar logiki warunkowej w widokach. Jako prosty przykład rozważmy sytuację, w której mamy dostępną metodę current_user, zwracającą aktualnie zalogowanego użytkownika. W plikach widoków często pojawiają się wtedy takie konstrukcje:

<h3>
Welcome,
<% if current_user %>
<%= current_user.name %>
<% else %>
Guest
<% end %>
</h3>

Lepszym rozwiązaniem jest upewnienie się, że obiekt zwracany przez current_user jest zawsze ustawiony, niezależnie od tego, czy ktoś jest zalogowany, oraz że w rozsądny sposób odpowiada na metody używane w widoku (nazywa się to czasem obiektem null). Na przykład helper current_user możesz zdefiniować w app/controllers/application_controller w ten sposób:

require 'ostruct'
helper_method :current_user

def current_user
  @current_user ||= User.find session[:user_id] if session[:user_id]
  if @current_user
    @current_user
  else
    OpenStruct.new(name: 'Guest')
  end
end

Pozwoliłoby to zastąpić poprzedni kod widoku jedną linijką:

<h3>Welcome, <%= current_user.name -%></h3>

Kilka dodatkowych rekomendacji dotyczących Rails:

Używaj layoutów i partiali, aby hermetyzować elementy powtarzające się na stronach.

Używaj prezenterów/dekoratorów, takich jak gem Draper, aby zamknąć logikę widoku w obiekcie Ruby. Następnie możesz dodać do tego obiektu metody wykonujące operacje logiczne, które w przeciwnym razie trafiłyby do kodu widoku.

Błąd 3. Zbyt dużo logiki w modelu

Skoro zalecenia mówią, by ograniczać logikę w widokach i kontrolerach, jedynym miejscem w MVC, gdzie można umieścić całą logikę, pozostaje model, prawda?

Cóż, nie do końca.

Wielu programistów Rails faktycznie popełnia ten błąd i ich klasy modeli ActiveRecord rozrastają się do ogromnych plików, które nie tylko łamią zasadę pojedynczej odpowiedzialności, ale są też koszmarem w utrzymaniu.

Funkcjonalności takie jak generowanie powiadomień e-mail, komunikacja z zewnętrznymi serwisami czy konwersja danych do innych formatów nie powinny być realizowane w modelach ActiveRecord, których zadaniem jest niewiele więcej niż wyszukiwanie i zapisywanie danych w bazie.

Ale jeśli taka logika nie powinna trafiać do widoku, kontrolera ani modelu — to gdzie?

Porozmawiajmy o „zwykłych starych obiektach Ruby” (POROs). Przy tak kompleksowym frameworku jak Rails mniej doświadczeni programiści często niechętnie tworzą własne klasy poza frameworkiem. Tymczasem przeniesienie logiki z modelu do POROs to często dokładnie to, czego trzeba, by uniknąć nadmiernie złożonych modeli. Dzięki POROs możesz zamknąć powiadomienia e-mail czy interakcje z API w osobnych klasach, zamiast trzymać je w modelu ActiveRecord.

Mając to na uwadze, jedyna logika, która powinna pozostać w modelu, to:

Konfiguracja ActiveRecord (np. relacje i walidacje).

Proste metody mutujące, hermetyzujące aktualizację kilku atrybutów i zapisanie ich w bazie.

Wrappery dostępu ukrywające wewnętrzne informacje modelu (np. metoda full_name łącząca pola first_name i last_name z bazy danych).

Złożone zapytania (bardziej skomplikowane niż zwykły find). Ogólnie rzecz biorąc, nie należy używać tej ani żadnej innej metody budowania zapytań poza samą klasą modelu.

Błąd 4. Śmietnik z ogólnych klas pomocniczych

Ten błąd jest poniekąd konsekwencją błędu 3. Jak już wspomniano, struktura Rails kładzie nacisk na nazwane komponenty (model, widok i kontroler) jako podstawę MVC. Istnieją dość jasne definicje tego, co należy do klas każdego z nich, ale czasem potrzebujemy metod, które nie pasują do żadnego z trzech.

Generatory Rails tworzą katalog helpers i nową klasę helpera. Bardzo kusi, by zacząć wrzucać tam funkcjonalność, która formalnie nie pasuje ani do modelu, ani do widoku, ani do kontrolera.

Choć Rails jest oczywiście frameworkiem skoncentrowanym na MVC, nic nie stoi na przeszkodzie, by tworzyć własne typy klas i dodawać odpowiednie katalogi na ich kod. Jeśli masz dodatkową funkcjonalność, zastanów się, które metody tworzą logiczne grupy, i znajdź dobre nazwy dla klas, które je będą zawierać. Korzystanie z Rails nie jest wymówką, by odsuwać na bok dobre praktyki projektowania obiektowego.

Błąd 5. Używanie zbyt wielu gemów

Ruby on Rails wspiera bogaty ekosystem gemów, które łącznie zapewniają praktycznie każdą funkcję, jaką programista może sobie wyobrazić. To bardzo wygodne przy szybkim budowaniu złożonych aplikacji, ale widziałem też wiele aplikacji, w których liczba gemów była nieproporcjonalnie duża w stosunku do oferowanej funkcjonalności.

Prowadzi to do kilku problemów. Nadużywanie gemów sprawia, że procesy Rails są większe, niż to konieczne. Może to spowolnić działanie aplikacji na produkcji. Poza frustracją użytkowników może to też oznaczać potrzebę większej ilości pamięci serwera, co zwiększa koszty operacyjne. Taka aplikacja dłużej się uruchamia, co spowalnia development i wydłuża testy automatyczne (a wolne testy z reguły uruchamia się rzadziej).

Pamiętaj, że każdy gem dodany do aplikacji zależy z kolei od innych gemów, a te od kolejnych i tak dalej. Dodawanie gemów ma więc efekt kumulacyjny. Na przykład dodanie gemu rails_admin dociągnie ponad 11 kolejnych gemów, co oznacza wzrost o ponad 10% względem bazowej instalacji Rails.

W chwili pisania tego artykułu świeża instalacja Rails 4.1.0 zawierała 43 gemy w pliku Gemfile.lock. To wyraźnie więcej niż w samym Gemfile — są tu wszystkie gemy, które standardowy zestaw gemów Rails dodaje jako zależności.

Za każdym razem, gdy dodajesz nowy gem, starannie rozważ dodatkowe koszty. Na przykład programiści często dodają gem rails_admin, bo zapewnia ładny interfejs webowy do struktury modeli. W rzeczywistości jest to jednak niewiele więcej niż efektowne narzędzie do przeglądania bazy danych. Nawet jeśli aplikacja wymaga administratorów z dodatkowymi uprawnieniami, prawdopodobnie nie chcesz dawać im bezpośredniego dostępu do bazy — lepiej opracować własny, prostszy panel administracyjny niż dodawać ten gem.

Błąd 6. Ignorowanie plików logów

Choć większość programistów Rails wie o domyślnych plikach logów dostępnych podczas developmentu, nie zwraca większej uwagi na zawarte w nich informacje. Wiele aplikacji na produkcji korzysta z narzędzi do monitorowania logów, takich jak Honeybadger czy New Relic, ale równie ważne jest śledzenie logów podczas tworzenia i testowania aplikacji.

Jak już wspomniano, Rails wykonuje za nas dużo „magii”, szczególnie w modelach. Asocjacje pozwalają bardzo łatwo budować relacje między tabelami i wyświetlać wszystko w widokach. Cały SQL generowany jest automatycznie. Ale skąd wiadomo, że wygenerowany SQL jest wydajny?

Jednym z przykładów, z którym będziesz się często spotykać, jest tzw. problem zapytań N+1. Choć jest on dobrze znany, jedynym sposobem, by go zauważyć, jest przejrzenie zapytań SQL w logach.

Załóżmy, że w typowej aplikacji blogowej masz następujące zapytanie, wyświetlające wszystkie komentarze dla wybranego zestawu postów:

def comments_for_top_three_posts
  posts = Post.limit(3)
  posts.flat_map do |post|
    post.comments.to_a
  end
end

Gdy zajrzymy do logu żądania, które wywołało tę metodę, zobaczymy mniej więcej coś takiego: jedno zapytanie pobrało trzy posty, a następnie trzy kolejne pobrały komentarze do każdego z nich:

Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700
Processing by PostsController#some_comments as HTML
Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3
Comment Load (5.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Rendered posts/some_comments.html.erb within layouts/application (12.5ms)
Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)

Active Record w Rails pozwala znacząco zmniejszyć liczbę zapytań, umożliwiając wcześniejsze wskazanie wszystkich relacji, które zostaną załadowane. Robi się to, wywołując metodę includes (lub preload) na obiekcie Arel (ActiveRecord::Relation). Dzięki includes ActiveRecord gwarantuje, że wszystkie wskazane relacje zostaną załadowane minimalną liczbą zapytań, na przykład:

def comments_for_top_three_posts
  posts = Post.includes(:comments).limit(3)
  posts.flat_map do |post|
    post.comments.to_a
  end
end

Po wykonaniu powyższego kodu widzimy w logach, że wszystkie komentarze zostały pobrane jednym zapytaniem zamiast trzech:

Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700
Processing by PostsController#some_comments as HTML
Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3
Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3)
Rendered posts/some_comments.html.erb within layouts/application (12.2ms)
Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)

Znacznie wydajniej.

Problem N+1 to tylko jeden przykład z wielu nieefektywności, które mogą czaić się „pod maską” aplikacji, jeśli nie poświęcasz temu wystarczającej uwagi. Wniosek z tego punktu: podczas developmentu sprawdzaj logi development i test, aby wychwycić (i naprawić!) nieefektywności w kodzie budującym zapytania.

Przeglądanie logów to świetny sposób na znalezienie nieefektywnego kodu i poprawienie go, zanim aplikacja trafi na produkcję. W przeciwnym razie nie możesz być pewny wydajności systemu, dopóki nie zacznie „żyć” — baza danych, z którą pracowałeś podczas developmentu i testów, będzie prawdopodobnie znacznie mniejsza od produkcyjnej. Nawet jeśli w nowej aplikacji produkcyjna baza jest na początku mała i wszystko działa dobrze, z czasem urośnie, a problemy opisane w tym przykładzie będą sprawiać, że system będzie działał coraz wolniej.

Jeśli zauważysz, że Twoje logi są zapchane mnóstwem niepotrzebnych informacji, możesz je wyczyścić.

Błąd 7. Brak testów automatycznych

Ruby on Rails domyślnie oferuje szerokie możliwości testowania automatycznego. Wielu programistów Rails pisze bardzo rozbudowane testy w stylu TDD i BDD oraz korzysta z jeszcze potężniejszych frameworków testowych dostarczanych w gemach (rspec i cucumber).

Mimo że dodanie testów automatycznych do aplikacji jest tak proste, byłem bardzo zaskoczony, gdy dołączałem do projektów, w których nie było napisanego dosłownie ani jednego testu (lub w najlepszym razie bardzo niewiele). Można się spierać, jak kompleksowe powinny być testy, ale jest zupełnie jasne, że przynajmniej jakieś testy automatyczne powinny istnieć w każdej aplikacji.

Ogólna zasada: dla każdej akcji kontrolerów powinien istnieć co najmniej jeden wysokopoziomowy test integracyjny. W przyszłości inni programiści zechcą rozszerzyć lub zmodyfikować kod albo zaktualizować wersję Ruby on Rails, a framework testowy pozwoli im upewnić się, że podstawowa funkcjonalność aplikacji działa. Dodatkową korzyścią jest to, że przyszli programiści otrzymują jasny obraz pełnego zakresu funkcjonalności aplikacji.

Błąd 8. Blokowanie na wywołaniach zewnętrznych serwisów

Zewnętrzne serwisy zazwyczaj bardzo łatwo integruje się z aplikacją Rails za pomocą gemów opakowujących ich API. Ale co się stanie, jeśli zewnętrzny serwis zacznie zawodzić lub działać bardzo wolno?

Aby uniknąć blokowania na takich wywołaniach, zamiast odwoływać się do nich bezpośrednio podczas obsługi żądania, przenoś je w miarę możliwości do zadań w tle. Kilka popularnych gemów:

Delayed job

Resque

Sidekiq

W przypadkach, gdy przeniesienie przetwarzania do tła jest niemożliwe lub niepraktyczne, upewnij się, że aplikacja odpowiednio obsługuje błędy i awarie, gdy zewnętrzny serwis „leży” lub ma problemy. Warto też przetestować aplikację bez zewnętrznych serwisów (np. odłączając od sieci serwer, na którym działa), aby upewnić się, że nie prowadzi to do nieprzewidzianych konsekwencji.

Błąd 9. Zbytnie przywiązanie do istniejących migracji bazy danych

Mechanizm migracji bazy danych w Rails pozwala tworzyć instrukcje, które automatycznie dodają lub usuwają tabele i wiersze. Ponieważ pliki migracji są nazywane sekwencyjnie, można je odtworzyć od samego początku, aby doprowadzić pustą bazę do tego samego schematu co na produkcji. To świetny sposób na zarządzanie drobnymi zmianami w schemacie bazy danych aplikacji.

Choć na początku projektu działa to dobrze, z czasem tworzenie bazy może trwać dość długo, a migracje czasem się gubią, są wstawiane w złej kolejności lub trafiają z innych aplikacji Rails korzystających z tego samego serwera.

Rails tworzy reprezentację bieżącego schematu w pliku db/schema.rb (domyślnie), który zwykle jest aktualizowany podczas uruchamiania migracji. Plik schema.rb można nawet wygenerować przy braku migracji, uruchamiając polecenie rake db:schema:dump. Częstym błędem jest zacommitowanie nowej migracji do repozytorium bez zaktualizowanego pliku schema.rb.

Gdy migracje wykonują się zbyt długo, programiści nie powinni bać się wyczyścić starego katalogu migracji, zrzucić nowy schemat i kontynuować od niego. Konfiguracja nowego środowiska developerskiego wymagałaby wtedy uruchomienia rake db:schema:load zamiast rake db:migrate, na którym polega większość programistów.

Niektóre z tych kwestii omawia także przewodnik Rails (Rails Guide).

Błąd 10. Commitowanie poufnych informacji do repozytorium kodu źródłowego

Framework Rails ułatwia tworzenie bezpiecznych aplikacji, chronionych przed wieloma rodzajami ataków. Częściowo osiąga się to za pomocą tajnego tokenu zabezpieczającego sesje przeglądarki. Choć obecnie token ten jest przechowywany w config/secrets.yml, a plik ten na serwerach produkcyjnych odczytuje token ze zmiennej środowiskowej, we wcześniejszych wersjach Rails token znajdował się w config/initializers/secret_token.rb. Plik ten jest często przez pomyłkę commitowany do repozytorium razem z resztą aplikacji. Jeśli tak się stanie, każdy, kto ma dostęp do repozytorium, może skompromitować wszystkich użytkowników Twojej aplikacji.

Dlatego upewnij się, że plik konfiguracyjny repozytorium (np. .gitignore dla użytkowników git) wyklucza plik z tokenem. Wtedy serwery produkcyjne mogą pobierać token ze zmiennej środowiskowej lub za pomocą mechanizmu takiego jak ten, który zapewnia gem dotenv.

Podsumowanie

Rails to potężny framework, który ukrywa wiele nieprzyjemnych szczegółów potrzebnych do zbudowania niezawodnej aplikacji. I choć Rails znacznie przyspiesza tworzenie aplikacji, programiści powinni zwracać uwagę na potencjalne błędy w kodzie i projekcie, aby ich aplikacje były łatwe w rozbudowie i utrzymaniu w miarę wzrostu.

Programiści powinni też być świadomi problemów, które mogą sprawić, że aplikacje staną się wolniejsze, mniej niezawodne lub mniej bezpieczne. Ważne jest, by poznać podstawy frameworka i upewnić się, że w pełni rozumiesz architekturę, projekt i kod na każdym etapie procesu tworzenia. Pozwoli to zbudować wysokiej jakości, wydajną aplikację.

Na podstawie materiałów ze stron: 

mkechinov.ru

toptal.com


Wstecz