Projektowanie OO w szynach: Gdzie umieścić rzeczy

244

Naprawdę lubię Railsy (chociaż ogólnie jestem RESTless) i lubię Ruby, która jest bardzo OO. Mimo to tendencja do tworzenia ogromnych podklas ActiveRecord i ogromnych kontrolerów jest całkiem naturalna (nawet jeśli używasz kontrolera na zasób). Gdybyś miał stworzyć głębsze światy obiektowe, gdzie umieściłbyś klasy (i moduły, jak sądzę)? Pytam o widoki (w samych Pomocnikach?), Kontrolery i modele.

Lib jest w porządku i znalazłem rozwiązania, które pozwolą go przeładować w środowisku deweloperskim , ale chciałbym wiedzieć, czy jest lepszy sposób na zrobienie tego. Naprawdę martwię się o zbyt duże klasy. A co z silnikami i jak się one mieszczą?

Dan Rosenstark
źródło

Odpowiedzi:

384

Ponieważ Rails zapewnia strukturę pod względem MVC, naturalne jest, że użyjesz tylko kontenerów modelu, widoku i kontrolera, które są dla Ciebie dostarczone. Typowym idiomem dla początkujących (a nawet niektórych pośrednich programistów) jest wbicie całej logiki aplikacji do modelu (klasy bazy danych), kontrolera lub widoku.

W pewnym momencie ktoś zwraca uwagę na paradygmat „grubego modelu, chudego kontrolera”, a pośredni programiści pośpiesznie usuwają wszystko ze swoich kontrolerów i wrzucają do modelu, który staje się nowym koszem na logikę aplikacji.

Chude kontrolery to w rzeczywistości dobry pomysł, ale następstwem - umieszczenie wszystkiego w modelu, nie jest tak naprawdę najlepszym planem.

W Ruby masz kilka dobrych opcji na uczynienie rzeczy bardziej modułowymi. Dość popularną odpowiedzią jest użycie modułów (zwykle ukrytych lib), które przechowują grupy metod, a następnie włączenie modułów do odpowiednich klas. Pomaga to w przypadkach, w których masz kategorie funkcjonalności, które chcesz ponownie wykorzystać w wielu klasach, ale funkcjonalność jest nadal teoretycznie dołączona do klas.

Pamiętaj, że kiedy dołączysz moduł do klasy, metody stają się metodami instancji klasy, więc nadal otrzymujesz klasę zawierającą mnóstwo metod, są one po prostu ładnie zorganizowane w wiele plików.

To rozwiązanie może działać dobrze w niektórych przypadkach - w innych przypadkach warto pomyśleć o użyciu klas w kodzie, które nie są modelami, widokami ani kontrolerami.

Dobrym sposobem na zastanowienie się nad tym jest „zasada pojedynczej odpowiedzialności”, która mówi, że klasa powinna być odpowiedzialna za jedną (lub niewielką liczbę) rzeczy. Twoje modele są odpowiedzialne za utrwalanie danych z aplikacji do bazy danych. Twoje kontrolery są odpowiedzialne za otrzymanie żądania i zwrócenie realnej odpowiedzi.

Jeśli masz koncepcje, które nie pasują do tych skrzynek (trwałość, zarządzanie żądaniami / odpowiedziami), prawdopodobnie zastanawiasz się, w jaki sposób modelowałbyś dany pomysł. Możesz przechowywać klasy nie modelowe w aplikacji / klasach lub w dowolnym innym miejscu i dodać ten katalog do ścieżki ładowania, wykonując:

config.load_paths << File.join(Rails.root, "app", "classes")

Jeśli używasz pasażera lub JRuby, prawdopodobnie chcesz również dodać swoją ścieżkę do chętnych ścieżek ładowania:

config.eager_load_paths << File.join(Rails.root, "app", "classes")

Najważniejsze jest to, że gdy dojdziesz do punktu w Railsach, w którym zadajesz to pytanie, nadszedł czas, aby pogłębić swoje kotlety Ruby i rozpocząć modelowanie klas, które nie są tylko klasami MVC, które Railsy domyślnie ci dają.

Aktualizacja: Ta odpowiedź dotyczy Railsów 2.x i nowszych.

Yehuda Katz
źródło
Nie. Dodanie osobnego katalogu dla nie-modeli nie przyszło mi do głowy. Czuję, jak nadchodzi porządek ...
Mike Woodhouse
Yehuda, dzięki za to. Świetna odpowiedź. Właśnie to widzę w aplikacjach, które dziedziczę (i tych, które tworzę): wszystko w kontrolerach, modelach, widokach i pomocnikach automatycznie udostępnianych dla kontrolerów i widoków. Potem są mixiny z lib, ale nigdy nie ma próby prawdziwego modelowania OO. Masz jednak rację: w „aplikacjach / zajęciach lub gdziekolwiek indziej”. Chciałem tylko sprawdzić, czy brakuje jakiejś standardowej odpowiedzi ...
Dan Rosenstark
33
W nowszych wersjach config.autoload_paths domyślnie przyjmuje wszystkie katalogi w aplikacji. Nie musisz więc zmieniać config.load_paths jak opisano powyżej. Nie jestem jednak pewien eager_load_paths (jeszcze) i muszę się temu przyjrzeć. Czy ktoś już wie?
Shyam Habarakada,
Biernie agresywny w stosunku do półproduktów: P
Sebastian Patten,
8
Byłoby miło, gdyby Railsy były dostarczane z tym folderem „klas”, aby zachęcić do „zasady pojedynczej odpowiedzialności” i umożliwić programistom tworzenie obiektów, które nie są wspierane przez bazę danych. Wydaje się, że implementacja „obaw” w Railsach 4 (patrz odpowiedź Simone) zadbała o wdrożenie modułów dzielących logikę między modelami. Jednak żadne takie narzędzie nie zostało utworzone dla zwykłych klas Ruby, które nie są wspierane przez bazę danych. Biorąc pod uwagę, że Rails jest bardzo opiniotwórczy, jestem ciekaw procesu myślowego stojącego za NIE obejmowaniem takiego folderu?
Ryan Francis
62

Aktualizacja : użycie obaw zostało potwierdzone jako nowe domyślne w Rails 4 .

To zależy od natury samego modułu. Zwykle umieszczam rozszerzenia kontrolera / modelu w folderze / dotyczy w aplikacji.

# concerns/authentication.rb
module Authentication
  ...
end    

# controllers/application_controller.rb
class ApplicationController
  include Authentication
end



# concerns/configurable.rb
module Configurable
  ...
end    

class Model 
  include Indexable
end 

# controllers/foo_controller.rb
class FooController < ApplicationController
  include Indexable
end

# controllers/bar_controller.rb
class BarController < ApplicationController
  include Indexable
end

/ lib jest moim preferowanym wyborem dla bibliotek ogólnego przeznaczenia. Zawsze mam przestrzeń nazw projektów w lib, w której umieszczam wszystkie biblioteki specyficzne dla aplikacji.

/lib/myapp.rb
module MyApp
  VERSION = ...
end

/lib/myapp/CacheKey.rb
/lib/myapp/somecustomlib.rb

Rozszerzenia rdzenia Ruby / Rails zwykle mają miejsce w inicjalizatorach konfiguracji, tak że biblioteki są ładowane tylko raz na boostrapie Rails.

/config/initializer/config.rb
/config/initializer/core_ext/string.rb
/config/initializer/core_ext/array.rb

W przypadku fragmentów kodu wielokrotnego użytku często tworzę wtyczki (mikro), aby móc ich ponownie używać w innych projektach.

Pliki pomocnicze zwykle zawierają metody pomocnicze, a czasem klasy, gdy obiekt jest przeznaczony do pomocy przez pomocników (na przykład Form Builderów).

To jest naprawdę ogólny przegląd. Podaj więcej szczegółów na temat konkretnych przykładów, jeśli chcesz uzyskać bardziej spersonalizowane sugestie. :)

Simone Carletti
źródło
Dziwna rzecz. Nie mogę uzyskać tej wymaganej zależności RAILS_ROOT + "/ lib / my_module" do pracy z czymś poza katalogiem lib. Zdecydowanie wykonuje i narzeka, jeśli plik nie zostanie znaleziony, ale go nie przeładuje.
Dan Rosenstark
Ruby wymaga tylko raz ładuje rzeczy. Jeśli chcesz załadować coś bezwarunkowo, użyj load.
Chuck
Wydaje mi się również dość niezwykłe, że chciałbyś dwukrotnie załadować plik podczas życia instancji aplikacji. Czy generujesz kod na bieżąco?
Chuck
Dlaczego korzystasz z parametru depend_dependency zamiast wymagania? Pamiętaj również, że jeśli przestrzegasz konwencji nazewnictwa, nie musisz wcale używać wymagania. Jeśli utworzysz MyModule w lib / my_module, możesz wywołać MyModule bez wcześniejszych wymagań (nawet jeśli użycie wymaga powinno być szybsze, a czasem bardziej czytelne). Zauważ też, że plik w / lib jest ładowany tylko raz na bootstrap.
Simone Carletti
1
Wykorzystanie obaw dotyczy
bbozo
10

... tendencja do tworzenia ogromnych podklas ActiveRecord i ogromnych kontrolerów jest całkiem naturalna ...

„ogromny” to niepokojące słowo ... ;-)

W jaki sposób kontrolery stają się ogromne? Na to powinieneś spojrzeć: idealnie, kontrolery powinny być cienkie. Wybierając ogólną regułę, sugeruję, że jeśli regularnie masz więcej niż, powiedzmy, 5 lub 6 linii kodu na metodę kontrolera (działanie), wtedy twoje kontrolery są prawdopodobnie zbyt tłuste. Czy istnieje duplikacja, która mogłaby przejść do funkcji pomocniczej lub filtra? Czy istnieje logika biznesowa, którą można by zepchnąć do modeli?

Jak twoje modele stają się ogromne? Czy powinieneś szukać sposobów na zmniejszenie liczby obowiązków w każdej klasie? Czy są jakieś typowe zachowania, które można wyodrębnić w miksy? Lub obszary funkcjonalności, które możesz delegować na klasy pomocnicze?

EDYCJA: Próbuję trochę rozszerzyć, mam nadzieję, że nie zniekształcisz niczego zbyt mocno ...

Pomocnicy: mieszkają app/helpersi służą głównie do uproszczenia widoków. Są albo specyficzne dla kontrolera (dostępne również dla wszystkich widoków dla tego kontrolera), albo ogólnie dostępne ( module ApplicationHelperw application_helper.rb).

Filtry: Powiedzmy, że masz ten sam wiersz kodu w kilku akcjach (dość często, wyszukiwanie obiektu za pomocą params[:id]lub podobnego). To duplikowanie można najpierw wyodrębnić do oddzielnej metody, a następnie całkowicie z akcji, deklarując filtr w definicji klasy, np before_filter :get_object. Patrz sekcja 6 w Poradniku ActionController Rails Niech deklaratywne programowanie będzie twoim przyjacielem.

Modele refaktoryzacji są nieco bardziej religijne. Uczniowie wuja Boba zasugerują na przykład, że przestrzegasz Pięciu Przykazań SOLID . Joel i Jeff mogą zalecić bardziej „erotyczne” podejście, chociaż później wydawało się, że są nieco bardziej pojednani . Znalezienie jednej lub więcej metod w klasie, które działają na jasno zdefiniowanym podzbiorze jej atrybutów, jest jednym ze sposobów próby zidentyfikowania klas, które mogą być refaktoryzowane z modelu opartego na ActiveRecord.

Nawiasem mówiąc, modele Railsów nie muszą być podklasami ActiveRecord :: Base. Innymi słowy, model nie musi być analogiem tabeli, ani nawet być powiązany z czymkolwiek przechowywanym. Nawet lepiej, jeśli tylko app/modelsnazwiesz swój plik zgodnie z konwencjami Railsów (wywołaj #underscore na nazwie klasy, aby dowiedzieć się, czego będzie szukał Rails), Railsy go znajdą bez requirepotrzeby.

Mike Woodhouse
źródło
To prawda pod każdym względem, Mike, i dziękuję za twoją troskę ... Odziedziczyłem projekt, w którym były pewne metody kontrolerów, które były ogromne. Podzieliłem je na mniejsze metody, ale sam kontroler wciąż jest „gruby”. Tak więc szukam wszystkich moich opcji do rozładowywania rzeczy. Twoje odpowiedzi to: „funkcje pomocnicze”, „filtry”, „modele”, „miksy” i „klasy pomocnicze”. Więc gdzie mogę umieścić te rzeczy? Czy mogę zorganizować hierarchię klas, która jest automatycznie ładowana w środowisku programistycznym?
Dan Rosenstark