Omówię tutaj przykład niewielkiego refaktoringu, który ostatnio robiliśmy przy okazji review jednego z pull requestów.
Zalety refaktoringu można śmiało tłumaczyć na przykładzie Świąt Wielkanocnych. Kiedy masz większą rodzinę i musisz obrać 30 jajek, może to być zadanie upierdliwe. Ale nie musi. Zależy to od tego, jak zostały ugotowane. Jeśli w słonej wodzie, a potem zalane zimną wodą – to cała praca nie zajmie więcej niż 15 minut, bo skorupka łatwo zejdzie. Natomiast jeśli trzeba się z tym babrać po małych, klejących się do białka łupinkach, to wyjdzie i godzina.
Przykład kodu przed zmianami
Pomyślmy o prostym modelu danych. Mamy dokument
, który posiada jedną lub więcej zgód
. Żeby nie upychać wszystkie w pliku modelu, robimy klasycznie klasę serwisową, która w tym przypadku ma się zajmować usuwaniem zbędnych, bo nie podpisanych zgód dokumentów.
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def destroy
destroy_agreements
end
private
def destroy_agreements
MultiJson.load(@agreements.to_json).each do |key, value|
next if value.present?
@document.granted_form_agreements.where(form_agreement_id: form_agreement_ids(key.to_s)).destroy_all
end
end
def form_agreement_ids(agreement_type)
FormAgreement.where(agreement_type: agreement_type).pluck(:id)
end
end
Co jest nie tak z tym kodem?
- Zbędne przetwarzanie JSON: W pierwszym przykładzie kodu, do przetwarzania obiektu
@agreements
użyto metodyto_json
iMultiJson.load
. Jednakże, nie ma potrzeby serializować i deserializować danych w tym przypadku, ponieważ@agreements
jest już tablicą. To zbędne przetwarzanie może wprowadzać dodatkowe koszty wydajnościowe. - Niepotrzebne metody pomocnicze: Użyto metody pomocniczej
form_agreement_ids
, która co prawda pomaga nam nazwać czym jest zmienna key, ale wcale nie ułatwia zadania. Do tego w linii 17 upchanych jest sporo metod, przez co gorzej się taki kod czyta i debuguje. - Brak optymalizacji zapytań do bazy danych: Każde wywołanie metody
destroy_all
powoduje osobne zapytanie do bazy danych. To może prowadzić do nadmiernego obciążenia bazy danych, szczególnie gdy operacja usuwania musi być wykonywana dla wielu elementów. Do tego mamy jeszcze dodatkowe zapytanie wykonujące się wielokrotnie na FormAgreement. - Niezwięzły kod: Kodu jest nieco nieczytelny w pętli, która iteruje po
@agreements
. Warunekif value.present?
może być mylący, ponieważ kod wewnątrz pętli wykonuje działania na obiektach, które nie spełniają tego warunku. Nie wiadomo też czym to value jest, ponieważ nie jest nazwane odpowiednią zmienną. - Jedna linia jest za długa: Zauważmy, że nie widać jej nawet na podglądzie. Linie prawie zawsze da się skrócić.
- Masło maślane: czyli nazewnictwo metody głównej
destroy
. Czasem się do zdarza przy robieniu builderów, że pisze siębuild
. Lepiej jest po prostu robićcall
.
Przykład kodu po zmianach, podejście pierwsze
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def call
destroy_agreements
end
private
def destroy_agreements
@agreements.each do |agreement_type, is_checked|
next if is_checked
@document
.granted_form_agreements
.joins(:form_agreement)
.where(form_agreements: { agreement_type: agreement_type })
.destroy_all
end
end
end
Zalety refaktoringu w tym przypadku obejmują:
- Uproszczenie kodu: Refaktoring dokonuje uproszczeń w strukturze kodu, co prowadzi do większej czytelności i zrozumienia. Po refaktoringu, kod jest bardziej zwięzły i klarowny, co ułatwia jego utrzymanie i rozwój w przyszłości.
- Usunięcie zbędnego przetwarzania: W oryginalnym kodzie używano metody MultiJson.load do serializacji i deserializacji obiektu
@agreements
. W zrewaktoryzowanym kodzie można bezpośrednio iterować po przekazanej tablicy@agreements
, eliminując tym samym zbędne przetwarzanie. - Optymalizacja zapytania do bazy danych: W zrewaktoryzowanym kodzie użyto łączenia (.joins) i warunku (where) bezpośrednio w zapytaniu do bazy danych, zamiast wywoływać dodatkową metodę
form_agreement_ids
. To pozwala na zredukowanie liczby zapytań do bazy danych i bardziej efektywne przetwarzanie danych.
Przykład kodu po zmianach, podejście drugie
Czy można chcieć więcej? Jak najbardziej.
class AgreementsDestroyer
def initialize(document:, agreements:)
@document = document
@agreements = agreements
end
def call
document
.granted_form_agreements
.joins(:form_agreement)
.where(form_agreements: { agreement_type: agreement_types_to_delete })
.destroy_all
end
private
attr_reader :document, :agreements
def agreement_types_to_delete
agreements.reject { |_type, is_checked| is_checked }.keys
end
end
Oto kilka zalet tej wersji:
- Prostsza logika: W tej wersji kodu, zamiast iterować po
@agreements
i sprawdzać wartośćis_checked
dla każdego typu umowy, użyto metody reject w celu odrzucenia umów, które nie są zaznaczone. To prowadzi do bardziej zwięzłej i czytelnej logiki. - Bezpośrednie pobieranie kluczy: Zamiast przekształcać
@agreements
do postaci tablicy kluczy, jak w drugim przykładzie, w tej wersji kodu klucze są bezpośrednio pobierane poprzez użycie metody keys na zwróconym wyniku z metody reject. To prowadzi do bardziej zwięzłego kodu. - Optymalizacja zapytań do bazy danych: Ta wersja kodu również wykorzystuje optymalizację zapytań do bazy danych, łącząc
granted_form_agreements
zform_agreements
i wykonując jedno zapytanie z warunkiem where. To zapewnia bardziej efektywne usuwanie umów, zwłaszcza w przypadku, gdyby trzeba było usuwać je często i w dużych ilościach. - Korzystanie z metod zwracających atrybuty instancji: Odwoływanie się do zmiennych instancji przez
@
jest czytelne w konstruktorze takiej klasy. Jednak robienie tego w dalszej części serwisu jest problematyczne, ponieważ literówka może zwrócić namnil
. Zresztą, można by i tutaj posunąć się o krok dalej i użyć metodyattr_accessor
, żeby ustawiać zmienne instancji w taki sposób:self.document = document
. Jednak przyjęło się, że w konstruktorze używamy@
. - Call robi robotę: Często zdarza się u nas w kodzie, że call niemalże aliasem do jakiejś prywatnej metody, w której jest logika serwisu. Wywoływanie poszczególnych metod w call ma sens, jeśli algorytm danej klasy jest obszerniejszy. W takim przypadku nie ma co mnożyć bytów. Z kolei prywatna metoda
agreement_types_to_delete
ma sens i niepotrzebnie wydłużałaby metodę call, gdyby jej wartość przypisywać do zmiennej.
Podsumowanie
Refaktoring kodu AgreementsDestroyer
jest dobrym przykładem dbałości o szczegóły i rozwijania umiejętności programistycznych. Chociaż może się wydawać, że drobne zmiany nie mają znaczenia, to właśnie na takich szczegółach polega różnica między dobrym a doskonałym kodem.
Dbałość o szczegóły w kodzie jest kluczowa dla jego czytelności, skalowalności i wydajności. Małe zmiany mogą znacząco wpływać na ostateczny rezultat i jakość oprogramowania.
Kiedy jesteś początkującym, zwracanie uwagi na takie szczegóły jest niezwykle ważne. Nie chodzi tu o małostkowość, ale o rozwijanie umiejętności analitycznego myślenia, refaktoryzacji kodu i optymalizacji. Poprawianie struktury kodu, usuwanie zbędnych operacji czy optymalizacja zapytań to umiejętności, które będą ci towarzyszyć przez całą karierę programisty.
Pamiętaj, że jako programista tworzysz oprogramowanie, które będzie wykorzystywane przez innych ludzi. Dbałość o szczegóły jest dowodem Twojego profesjonalizmu. Wielkie rzeczy zaczynają się od małych detali, dlatego warto zwracać uwagę na każdy aspekt kodu i dążyć do doskonałości. Zwłaszcza w sobotnie poranki, kiedy kawa smakuje najlepiej.
(Uwielbiam jak ChatGPT podsumowuje przekaz o refaktoringu w swoim entuzjastycznym AI Duchu).