Witaj na blogu prowadzonym przez Sebana. Spisuję tutaj swoje uwagi na różny temat. Przeważają tematy związane z Internetem, popieranymi przeze mnie rozwiązaniami dotyczącymi wykorzystania komputerów, oraz kilka innych. Przeczytasz tu również recenzje książek IT.

Powtarzaj DRY

12 września 2007 | Klucze: programowanie, rails, Techblog
2 komentarze. trackback

Kto zna już Rails? Kto jeszcze nie zna? Dobra, dobra. Pytanie właściwie nie ma znaczenia bo dzisiejszy wpis będzie i dla jednych i dla drugich. Jak wiele lini kodu się powtarza w źródle twoich klas? Jak wiele z nich umieściłeś w osobnej metodzie? Dziś napiszę jak chociaż trochę w prosty sposób uporządkować kontroler. To dalej bo szkoda czasu.

Nazewnictwo

Łatwo sobie wyobrazić jakis serwis z podcastami. Są ludzie dodają sobie swoje podcasty, komentują je, tagują i co tam jeszcze można robić teraz w serwisach internetowych ery 2.0. Ale zaimijmy się bardziej szczegółowo podcastem. Co użytkownik może z nim zrobić: może go dodać, może go potem edytować, by poprawić jakieś literówki, inni mogą przeglądać szczegóły podcasu: jego opis i wszystko inne co ma opisywać podcast. Każdy chce mieć wartością treść w swoim serwisie, więc przyda nam się jakiś moderator by usuwał to co jest złe. Jakieś bluzgi czy bekanie do mikrofonu. Cztery proste czynności tworzenie, usuwanie, przeglądanie, edycja. Na ilę sposóbów mogę nazwać akcję tworzącą nowy podcast? Nawet ograiczając się do angielskiego bardzo łatwo podać kilka słów oznaczających prawie to samo: new, add, create. A jeśli w takim kontrolerze są wszystkie trzy nazywy to która tak napawdę tworzy nowy podcast w serwisie? DHH mówił o tym problemie a ja chcę powtórzyć. Sięgnij po CRUD. Zatem nazywajmy zawsze te podstawowe metody tak samo: create, show, update, destroy.

Pozwoliłem sobie dodać już częściową implementację tych akcji, brakuje tylko create.

Uporządkujmy

Pół biedy jeśli to wygląda tak jak na załączonym wyżej obrazku. Dużo gorzej jeśli na początku każdej z metod jest:

Przynajmniej trzy razy piszesz to samo! Mój sposób by sobie z tym poradzić to before_filter. Piszę sobie metodę: find_podcast

Spokojnie, spokojnie, nie podpalać się i nie wpisywać tej metody na początku każdej z metod. To też bez sensu. Pamiętajcie o before_filter.

To według mnie jest DRY. Eliminacja powtarzającego się kodu, oszczędzanie palców czy jak jeszcze to nazwać. Paradoksalnie na napisanie find_podcast użyłem więcej linii niż zaoszczędziłem, ale według mnie tak jest estetyczniej, czytelniej. Ale DRY to nie tylko pisanie metod podobnych do find_podcast, to również ,,widoki częściowe'' - partiale (o których pisałem już na Jabbie), oraz metody pomocnicze.

Postanowiłem uzupełnić kod kontrolera jeszcze o metodę list, która będzie pokazywała listę wszystkich podcastów. Teraz find_podcast będzie wywoływane przed każdą metodą oprócz create.

Spostrzegawczy lub tacy, którzy już pisali w RoR powiedzą: ,,no dobra, ale teraz każdy może usunąć podcast''. Tak w tym momencie każdy może usunąć podcast. A nawet więcej! Teraz użytkownik może edytować wszystkie podcasty. Wolna amerykanka! A co jeśli nie uda się zapisać podcastu? Ale to jeśli będzie ktoś chciał przeczytać mogę napisać w sobotę.

Literatura

Na sam koniec przydatne artykuły, video czy textareazentacje:


KOMENTARZE

17 września 2007 | Radarek |

Hm. DRY to świetna idea, a jak każda idea wymaga dogłębnego zrozumienia. Muszę przyznać, że jeszcze niedawno podpisałbym się pod tym co napisałeś, ale teraz chyba już nie… DRY – nie powtarzaj się, to każdy wie. Jednak tu nie chodzi o to, że w ogóle nie można się powtarzać. Chodzi o to, aby pewne części systemu (aplikacji) nie były zdefiniowane, zdublowane w dwóch różnych miejscach. Jeśli zmieni się coś w jednym miejscu, a zapomni w tym drugim to zapewne powstanie błąd. Przykładowo (na forum.rubyonrails.pl było kiedyś takie pytanie), masz metodę current_user (zwraca bieżącego zalogowanego usera) w kontrolerze. Chciałbyś tą samą metodę w widoku, ale jak wiadomo metody kontrolerów tam nie są widoczne. Musiałbyś zdefiniować tą metodę jeszcze raz w helperze. Jeśli zmieni się jedna to druga też powinna (są takie same!). Na szczęście można użyć helper_method w kontrolerze i wyeksponować ją do helpera.

Wracając do Twojego przykładu. Próbujesz traktować DRY zbyt dosłownie (tak jak mówię, ja chciałem robić na początku dokładnie tak samo). Zauważ, że Twój kod kontrolera jest kodem końcowym, tzn sam nigdzie nie zostanie już użyty, a więc można powiedzieć, że żadna część systemu od niego nie jest zależna. Dodatkowo zauważ, że (IMHO) bardziej sobie skomplikowałeś kod, gdyż musisz skapować skąd się bierze poprawny obiekt Podcast w zmiennej @podcast.

Każda z twoich akcji, które wymaga znalezienia podcasta po jego id ma taką logikę:
1. znajdź podcast po jego id = params[:id] i zapamiętaj ją w zmiennej @podcast
2. zrób coś z podcastem

I tak jak mówię, nie ma tu co cudować, logiki tej nie zmienisz i lepiej zostawić wersję bez filtrów. Ale podam Ci przykład, w którym (IMHO) DRY powinien być zastosowany. Załóżmy, że użytkownicy dodają podcasty, ale zanim pojawią się na stronie głównej, musi je zaakceptować (flaga accepted = true). Zatem w metodzie list mógłbyś napisać:

@podcasts = Podcast.find(:all, :conditions => [‘accepted = ?’, true])

I teraz jeśli w innym miejscu chcesz pobrać tylko zaakceptowane podcasty (np. RssController) powtórzysz dokładnie ten same finder z tymi samymi warunkami. A co, jeśli zmienisz flagą accepted na jakąś inną? Lepiej jest wtedy zrobić osobną metodę klasową w Podcas:

class Podcast < ActiveRecord::Base def self.find_accepted return find(:all, :conditions => [‘accepted = ?’, true]) end
end

To moim zdaniem DRY. DRY to także wszystkie wartości, które stanowią konfigurację systemu (zmienna którą zmieniasz w jednym miejscu i pozostała część systemu zależna od niej widzi te zmiany, np. adres jakiejś usług, urle itp). Na pewno można podać więcej przykładów. Aczkolwiek tak jak ze wszystkim, trzeba uważać żeby nie przesadzić :).

15 grudnia 2009 | Greg |

Jasne, ale moim zdaniem Seban pokazał tu ważną rzecz, że trzeba starać się stosować DRY. Zrozumienie poprawnego DRY i nieodpowiedniego przychodzi z czasem.

Poza tym dla każdego DRY oznacza coś innego. W naszym aktualnym projekcie jako pochodną DRY stosowaliśmy skinny controller, fat model. Na początku uznaliśmy, że każdego bardziej skomplikowanego finda należy wywalić do modelu. Okazało się po jakimś czasie, że zmniejsza to czytelność kodu - zamiast frameworkowego find'a z conditions miałeś ModelX::getcośtam(parametr). Okazało się, że jest to szkodliwe też z drugiego powodu (wspomniany w komentarzu powyżej). Otóż gdy taki getcośtam jest używany w kilku akcjach i nagle okazuje się, że w jednej z nich potrzeba odwrotnej kolejności zwracanych wyników.
Pojawił się dylemat: rozbudowywać get_cośtam o parametr order?

W każdym razie zmierzam do tego, że trzeba starać się stosować DRY, myśleć o nim i mieć to w głowie. Jeśli ma się dość oleju w głowie to po jakimś czasie przyjdzie zrozumienie jakie DRY jest dobre. Po prostu nie da się inaczej wytłumaczyć kiedy przeprowadzić ekstrakcję danego fragmentu do osobnej metody, a kiedy nie. Trzeba po prostu kilka razy się pomylić i poczuć problemy jakie z tego wynikają...