Filtry i bałagan w kodzie

Mam takie przykładowe działanie aplikacji.

Użytkownik komentuje wpis. Po zapisaniu, model wpisu uruchamia ater_save, w którym tworzę event z informacją kto i co. Po zapisaniu eventu, uruchamia się after_save, tym razem modelu event, który …
Zauważyłem, że coraz więcej mi się takich łańcuszków robi (jeden model tworzy w after_save drugi model, ktory w swoim after_save tworzy trzeci, albo cos w tym stylu). Na razie mam mały projekt, a już zaczynam się czasami w nich gubić, bo trzeba skakać po plikach i uważać, żeby nie zrobiła się gdzieś pętla. Zastanawiałem się czy nie lepiej np. zrobić to wszystko od razu w pierwszym modelu.

Dobrze, że już w tym momencie do tego doszedłeś :wink: To zależy od architektury ale moim zdaniem używanie filtrów gdzie się da to droga donikąd - w dużych projektach może się bardzo zemścić. Zastanów się, może jakieś wzorce przyjdą z pomocą :slight_smile:

Możesz stworzyć service object: http://railscasts.com/episodes/398-service-objects, np:

[code=ruby]class CommentService
def initialize(payload)

end

def create
Comment.create(comment)
Event.create(…)
Other.create(…)
end
end[/code]

after_save to zdecydowanie nie jest miejsce na tego typu zabawy. Jeśli musisz stworzyć wiele powiązanych obiektów, to niech to się dzieje w osobnej klasie (może być to CommentService, ale ja bym to inaczej nazwał). Napisz testy dla różnych scenariuszy tworzenia commentów. No i pomyśl o pozbyciu się twardych odwołań do klas (typu Comment.create, Event.create, etc.) To Ci ułatwi testowanie.

CommentCreator?

Mógłbyć podać jakiś przykład jak się pozbyć takich rzeczy?

To zabrzmi groźnie - dependency injection. Temat jest wałkowany również na naszym forum (ostatnio pisał o tym paneq w kontekście walidacji), ale żeby nie powtarzać rzeczy już powiedzianych, to polecam:

  1. Avdi Grimm - Objects on Rails - solidne wprowadzenie do tego jak można dobrze pisać apkę Railsową korzystając z TDD
  2. Dependor - framework DI dla Rubiego. Nie do końca jestem przekonany, że jest niezbędny, ale można popatrzeć na przykłady i samemu zdecydować.

A sam kod też mogę podać - ale bez odpowiedniego kontekstu to będzie za mało

[code=ruby]class CommentCreator
def initalize(comment_factor=Comment,event_factory=Event)
@comment_factor = comment_factory
@event_factory = event_factory
end

def create(params)
comment = @comment_factory.new(params[:comment])
event = @event_factory.new(params[:event])
# …
end
end

Testy

describe CommentCreator do
subject(:creator) { CommentCreator.new(comment_factory,event_factory) }
let(:comment_factory) { stub }
let(:event_factory) { stub }
let(:comment) { … }
let(:event) { … }

it “should create comments” do
mock(comment_factory).new(title: “Title”) { comment }
mock(event_factory).new(date: Time.now) { event }

creator.create(comment: {title: "Title"}, event: {date: Time.now })

end
end[/code]
Można tam oczywiście te parametry również przesunąć gdzieś do zmiennych za pomocą let. Ale to nie jest najważniejsze - najistotniejsze jest przekazanie odpowiednich fabryk np. w konstruktorze. Wtedy w testach możemy podmienić to co w realnej implementacji jest ustawione domyślnie na Comment i Event.

Ja niezbyt lubie (też nie próbowałem) używać dependency injection w takich przypadkach, jak powyższy, gdzie nie ma dużo logiki biznesowej, a jest tylko logika tworzenia obiektów. Dopiero gdy logika tworzenia obiektu(ów) robi się skomplilkowana, to tworzę klasy, które nie posiadają zależności od ActiveRecord.

A czym się różni jedno o drugiego? To że się jakiś event tworzy pododaniu komentarza to zdecydowanie logika biznesowa. Polecam poczytać Objects on Rails. Oczywiście wielu uzna, że to przerost formy nad treścią. Dopóki nie napotka systemu, w którym wprowadzenie zmiany będzie trudniejsze niż napisanie połowy systemu od nowa.

można też wrzucić do observera

Co tak naprawdę nic nie zmienia. No, poza przeniesieniem kodu z jednego miejsca w inne.

Jak dla mnie to chyba za wysokie progi na razie (a ta książka to już w ogóle, po kilku stronach niestety musiałem odpuścić). Rozumiem, że po prostu całą logikę obsługi tego co się dzieje przy tworzeniu nowego komentarza (inicjowanie nowych modeli, zapisanie ich itd.) wydzielamy poza model Comment.

W powyższym przypadku ja tak uważam.

Nie trzeba od razu tworzyć kodu w stylu Objects on Rails aby mieć czysty, łatwy do zmian kod.

Nie trzeba od razu tworzyć kodu w stylu Objects on Rails aby mieć czysty, łatwy do zmian kod.[/quote]
Tylko jak już przyjdzie na to czas, to trudno będzie to zrobić post factum.
Ze względu na prostotę Rubiego (domyślne parametry, klasy są od początku fabrykami), dodanie DI w takiej postaci jak w Object on Rails jest prawie bezkosztowe. Narzut polega głównie na użyciu @comment_factory.new zamianst Comment.new. Czy to jest aż taka różnica? IMHO nie. Czy ułatwia testowanie i zmiany? Bardzo!