Buduj głupie, refaktoruj mądrze: jak usunąć problemy z kodu Ruby on Rails
Opublikowany: 2022-03-11Czasami klienci przekazują nam prośby o nowe funkcje, których tak naprawdę nie lubimy. Nie chodzi o to, że nie lubimy naszych klientów — kochamy naszych klientów. Nie chodzi o to, że nie podoba nam się ta funkcja — większość funkcji, o które proszą klienci, jest idealnie dopasowana do ich celów biznesowych i dochodów.
Czasami nie lubimy prośby o nową funkcję, ponieważ najłatwiejszym sposobem jej rozwiązania jest napisanie złego kodu, a my nie mamy eleganckiego rozwiązania na czubku głowy. To spowoduje, że wielu z nas, deweloperów Railsów, zacznie bezowocnie przeszukiwać RubyToolbox, GitHub, blogi deweloperów i StackOverflow w poszukiwaniu klejnotu, wtyczki lub przykładowego kodu, który sprawi, że poczujemy się lepiej.
Refaktoryzacja kodu Ruby on Rails
Cóż, jestem tutaj, aby ci powiedzieć: można pisać zły kod. Czasami zły kod Railsowy jest łatwiej przerobić na piękny kod niż źle przemyślane rozwiązanie zaimplementowane w czasie kryzysu.
Oto proces refaktoryzacji Railsów, który lubię stosować, gdy masuję problemy z moich okropnych rozwiązań w zakresie pomocy bandażowej:
Dla alternatywnej perspektywy, oto dziennik zatwierdzenia Git dla funkcji, która została zrefaktoryzowana krok po kroku:
A oto kolejny ciekawy artykuł o refaktoryzacji na dużą skalę od kolegi z sieci Toptal.
Zobaczmy, jak to się robi.
Widoki
Krok 1. Zacznij od Widoków
Załóżmy, że rozpoczynamy sprzedaż biletów na nową funkcję. Klient mówi nam: „Odwiedzający powinni mieć możliwość wyświetlenia listy aktywnych projektów na stronie powitalnej”.
Ten bilet wymaga widocznej zmiany, więc rozsądnym miejscem do rozpoczęcia pracy byłyby Widoki. Problem jest prosty i każdy z nas wielokrotnie szkolił się w jego rozwiązywaniu. Zamierzam rozwiązać ten problem The Wrong Way i zademonstrować, jak przerobić moje rozwiązanie na odpowiednie obszary. Rozwiązywanie problemu Niewłaściwa droga może pomóc nam przezwyciężyć garb nieznajomości właściwego rozwiązania.
Na początek załóżmy, że mamy model o nazwie Project
z atrybutem logicznym o nazwie active
. Chcemy uzyskać listę wszystkich Projects
, w których active
jest równe true
, więc możemy użyć Project.where(active: true)
i zapętlić go z each
blokiem.
app/views/pages/welcome.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
Wiem, co mówisz: „To nigdy nie przeszłoby przeglądu kodu” lub „Mój klient na pewno by mnie za to zwolnił”. Tak, to rozwiązanie przełamuje separację Model-Widok-Kontroler, może skutkować zabłąkanymi wywołaniami bazy danych, które są trudne do śledzenia i może stać się trudne do utrzymania w przyszłości. Ale zastanów się nad wartością robienia tego w zły sposób :
- Tę zmianę można uzyskać na inscenizacji w mniej niż 15 minut.
- Jeśli zostanie pozostawiony, blok ten można łatwo buforować.
- Naprawienie tego problemu z Railsami jest proste (może zostać przekazane młodszemu programiście).
Krok 2. Częściowe
Po zrobieniu tego Niewłaściwie czuję się źle ze sobą i chcę odizolować mój zły kod. Gdyby ta zmiana wyraźnie dotyczyła tylko warstwy Widok, mógłbym zmienić swój wstyd na częściowy.
app/views/pages/welcome.haml: = render :partial => 'shared/projects_list' app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| %li.project= link_to project_path(project), project.name
Tak jest trochę lepiej. Oczywiście nadal popełniamy błąd zapytania Model w widoku, ale przynajmniej gdy opiekun wejdzie później i zobaczy mój okropny fragment, będzie miał prosty sposób na rozwiązanie tego problemu w Railsach. Naprawienie czegoś wyraźnie głupiego jest zawsze łatwiejsze niż naprawienie źle zaimplementowanej, błędnej abstrakcji.
Krok 3. Pomocnicy
Helpery w Railsach są sposobem na tworzenie DSL (języka specyficznego dla domeny) dla sekcji twoich widoków. Musisz przepisać swój kod, używając tagów content_tag zamiast haml lub HTML, ale zyskujesz możliwość manipulowania strukturami danych bez konieczności rzucania okiem innym programistom na 15 wierszy niedrukowalnego kodu widoku.
Gdybym miał używać tutaj pomocników, prawdopodobnie zmieniłbym tag li
.
app/views/shared/projects_list.haml: %ul.projects - Project.where(active: true).each do |project| = project_list_item(project) app/helpers/projects_helper.rb: def project_list_item(project) content_tag(:li, :class => 'project') do link_to project_path(project), project.name end end
Kontrolerzy
Krok 4. Przenieś go do kontrolerów
Może twój okropny kod nie jest tylko problemem View. Jeśli twój kod nadal pachnie, poszukaj zapytań, które możesz przenieść z widoków do kontrolerów.

app/views/shared/projects_list.haml: %ul.projects - @projects_list.each do |project| = project_list_item(project) app/controllers/pages_controller.rb: def welcome @projects = Project.where(active: true) end
Krok 5. Filtry kontrolera
Najbardziej oczywistym powodem przeniesienia kodu do kontrolera before_filter
lub after_filter
jest kod, który został zduplikowany w wielu akcjach kontrolera. Możesz również przenieść kod do filtru Kontroler, jeśli chcesz oddzielić cel akcji Kontroler od wymagań Twoich widoków.
app/controllers/pages_controller.rb: before_filter :projects_list def welcome end def projects_list @projects = Project.where(active:true) end
Krok 6. Kontroler aplikacji
Załóżmy, że potrzebujesz kodu, aby pojawiał się na każdej stronie lub chcesz udostępnić funkcje pomocnicze kontrolera wszystkim kontrolerom, możesz przenieść swoją funkcję do ApplicationController. Jeśli zmiany są globalne, możesz również zmodyfikować układ aplikacji.
app/controllers/pages_controller.rb: def welcome end app/views/layouts/application.haml: %ul.projects - projects_list.each do |project| = project_list_item(project) app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.where(active: true) end
Modele
Krok 7. Refaktoryzacja do modelu
Zgodnie z mottem MVC: Fat Model, Skinny Controllers i Dumb Views . Oczekuje się, że dokonamy refaktoryzacji wszystkiego, co możemy, do modelu i prawdą jest, że najbardziej złożone funkcje w końcu staną się powiązaniami modeli i metodami modelowymi. Powinniśmy zawsze unikać formatowania/wyświetlania rzeczy w Modelu, ale przekształcanie danych w inne typy danych jest dozwolone. W takim przypadku najlepszą rzeczą do refaktoryzacji do modelu byłaby klauzula where(active: true)
, którą możemy przekształcić w zakres. Korzystanie z zakresu jest cenne nie tylko dlatego, że sprawia, że wywołanie wygląda ładniej, ale jeśli kiedykolwiek zdecydujemy się dodać atrybut, taki jak delete
lub outdated
, możemy zmodyfikować ten zakres zamiast szukać wszystkich naszych klauzul where
.
app/controllers/application_controller.rb: before_filter :projects_list def projects_list @projects = Project.active end app/models/project.rb: scope :active, where(active: true)
Krok 8. Filtry modelu
W tym przypadku nie mamy żadnego szczególnego zastosowania dla filtrów before_save
lub after_save filters
modelu, ale następnym krokiem, który zwykle wykonuję, jest przeniesienie funkcjonalności z metod Controllers i Model do filtrów Model.
Załóżmy, że mamy inny atrybut, num_views
. Jeśli num_views > 50
, projekt staje się nieaktywny. Moglibyśmy rozwiązać ten problem w widoku, ale wprowadzanie zmian w bazie danych w widoku jest niewłaściwe. Moglibyśmy to rozwiązać w kontrolerze, ale nasze kontrolery powinny być jak najcieńsze! Możemy to łatwo rozwiązać w Modelu.
app/models/project.rb: before_save :deactivate_if_over_num_views def deactivate_if_over_num_views if num_views > 50 self.active = false fi end
Uwaga: należy unikać wywoływania self.save
w filtrze Model, ponieważ powoduje to rekurencyjne zdarzenia zapisu, a warstwa manipulacji bazą danych aplikacji powinna i tak być kontrolerem.
Krok 9. Biblioteki
Czasami twoja funkcja jest na tyle duża, że może zagwarantować jej własną bibliotekę. Możesz chcieć przenieść go do pliku biblioteki, ponieważ jest on ponownie używany w wielu miejscach lub jest na tyle duży, że chciałbyś go tworzyć osobno.
Można przechowywać pliki bibliotek w katalogu lib/ , ale w miarę ich wzrostu możesz przenieść je do prawdziwego RubyGem! Główną zaletą przeniesienia kodu do biblioteki jest możliwość testowania biblioteki niezależnie od modelu.
W każdym razie, w przypadku listy projektów, moglibyśmy uzasadnić przeniesienie wywołania scope :active
z modelu Project
do pliku biblioteki i przywrócenie go z powrotem do Rubiego:
app/models/project.rb: class Project < ActiveRecord::Base include Activeable before_filter :deactivate_if_over_num_views end lib/activeable.rb: module Activeable def self.included(k) k.scope :active, k.where(active: true) end def deactivate_if_over_num_views if num_views > 50 self.active = false end end end
Uwaga: metoda self.included
jest wywoływana, gdy klasa Rails Model jest ładowana i przekazuje zakres klasy jako zmienną k
.
Wniosek
W tym samouczku dotyczącym refaktoryzacji Ruby on Rails zajęliśmy mniej niż 15 minut i zaimplementowaliśmy rozwiązanie i umieściliśmy je na etapie testowania użytkowników, gotowe do przyjęcia do zestawu funkcji lub usunięcia. Pod koniec procesu refaktoryzacji mamy fragment kodu, który określa strukturę implementacji elementów z możliwością listy i aktywowania w wielu modelach, które przeszłyby nawet najbardziej rygorystyczny proces recenzji.
W swoim własnym procesie refaktoryzacji Railsów, możesz pominąć kilka kroków w dół potoku, jeśli masz pewność, że to zrobisz (np. przeskocz z widoku do kontrolera lub z kontrolera do modelu). Pamiętaj tylko, że przepływ kodu odbywa się od widoku do modelu.
Nie bój się wyglądać głupio. To, co odróżnia nowoczesne języki od starych aplikacji do renderowania szablonów CGI, nie polega na tym, że za każdym razem robimy wszystko we właściwy sposób — chodzi o to, że poświęcamy czas na refaktoryzację, ponowne wykorzystanie i dzielenie się naszymi wysiłkami.