Alternatywy dla Rails Observer dla 4.0

154

Po oficjalnym usunięciu Observers z Rails 4.0 jestem ciekawy, czego używają inni programiści w ich miejsce. (Poza używaniem wydobytego klejnotu.) Podczas gdy obserwatorzy byli z pewnością wykorzystywani i czasami mogli łatwo stać się nieporęczni, było wiele przypadków użycia poza zwykłym czyszczeniem pamięci podręcznej, w których były one korzystne.

Weźmy na przykład aplikację, która musi śledzić zmiany w modelu. Obserwator może łatwo obserwować zmiany w Modelu A i rejestrować te zmiany w Modelu B w bazie danych. Jeśli chcesz obserwować zmiany w kilku modelach, pojedynczy obserwator może sobie z tym poradzić.

W Rails 4 jestem ciekaw, jakie strategie używają inni programiści zamiast Observers, aby odtworzyć tę funkcjonalność.

Osobiście skłaniam się ku pewnego rodzaju implementacji „grubego kontrolera”, w której te zmiany są śledzone w metodzie tworzenia / aktualizacji / usuwania kontrolera każdego modelu. Chociaż nieznacznie powiększa zachowanie każdego kontrolera, pomaga w czytelności i zrozumieniu, ponieważ cały kod znajduje się w jednym miejscu. Wadą jest to, że istnieje teraz bardzo podobny kod rozproszony w kilku kontrolerach. Wyodrębnienie tego kodu do metod pomocniczych jest opcją, ale nadal pozostają wszędzie wywołania tych metod. To nie koniec świata, ale też niezupełnie w duchu „chudych kontrolerów”.

Wywołania zwrotne ActiveRecord to kolejna możliwa opcja, chociaż osobiście nie lubię, ponieważ moim zdaniem ma tendencję do łączenia dwóch różnych modeli zbyt blisko siebie.

Więc w świecie Rails 4, no-Observers, gdybyś musiał stworzyć nowy rekord po utworzeniu / aktualizacji / zniszczeniu innego rekordu, jakiego wzorca projektowego byś użył? Grube kontrolery, wywołania zwrotne ActiveRecord czy coś zupełnie innego?

Dziękuję Ci.

kennyc
źródło
4
Jestem naprawdę zaskoczony, że nie ma więcej odpowiedzi na to pytanie. Trochę niepokojące.
courimas

Odpowiedzi:

82

Spójrz na obawy

Utwórz folder w katalogu modeli o nazwie obawy. Dodaj tam moduł:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Następnie uwzględnij to w modelach, w których chcesz uruchomić after_save:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

W zależności od tego, co robisz, może to Cię zbliżyć bez obserwatorów.

Wujek Adam
źródło
20
Z takim podejściem są problemy. Warto zauważyć, że nie czyści twoich modeli; include kopiuje metody z modułu z powrotem do Twojej klasy. Wyodrębnianie metod klasowych do modułu może pogrupować je według obaw, ale klasa jest nadal tak samo rozdęta.
Steven Soroka
15
Tytuł brzmi „Rails Observer Alternatives for 4.0”, a nie „Jak zminimalizować wzdęcia”. Jak to się dzieje, że obawy nie działają, Steven? I nie, sugerowanie, że `` wzdęcie '' jest powodem, dla którego to nie działa jako zamiennik obserwatorów, nie jest wystarczająco dobre. Będziesz musiał wymyślić lepszą sugestię, aby pomóc społeczności lub wyjaśnić, dlaczego obawy nie zastąpią obserwatorów. Miejmy nadzieję, że podasz oba = D
UncleAdam
10
Wzdęcie jest zawsze problemem. Lepszą alternatywą jest rozwidlenie , które, jeśli jest odpowiednio zaimplementowane, pozwala uporządkować problemy poprzez wyodrębnienie ich do oddzielnych klas, które nie są ściśle powiązane z modelami. To również znacznie ułatwia testowanie w izolacji
Steven Soroka
4
Modeluj wzdęcie lub wzdęcie całej aplikacji, pociągając w tym celu Gem - możemy pozostawić to indywidualnym preferencjom. Dzięki za dodatkową sugestię.
UncleAdam
Spowodowałoby to tylko powiększenie menu autouzupełniania metody IDE, co powinno być dobre dla wielu ludzi.
lulalala
33

Są teraz we wtyczce .

Czy mogę również polecić alternatywę, która da ci kontrolery takie jak:

class PostsController < ApplicationController
  def create
    @post = Post.new(params[:post])

    @post.subscribe(PusherListener.new)
    @post.subscribe(ActivityListener.new)
    @post.subscribe(StatisticsListener.new)

    @post.on(:create_post_successful) { |post| redirect_to post }
    @post.on(:create_post_failed)     { |post| render :action => :new }

    @post.create
  end
end
Kris
źródło
A co z ActiveSupport :: Notifications?
svoop
@svoop ActiveSupport::Notificationssą nastawione na instrumentację, a nie ogólne sub / pub.
Kris
@Kris - masz rację. Jest używany głównie do instrumentacji, ale zastanawiam się, co zapobiega używaniu go jako ogólnej metody dla pub / sub? zapewnia podstawowe elementy budulcowe, prawda? Innymi słowy, jakie są wady / zalety w porównaniu z mądrością ActiveSupport::Notifications?
gingerlime
Nie używałem Notificationsdużo, ale powiedziałbym, że Wisperma ładniejszy interfejs API i funkcje, takie jak „globalni subskrybenci”, „na prefiksie” i „mapowanie zdarzeń”, które Notificationstego nie robią. Przyszłe wydanie Wisperumożliwi również asynchroniczne publikowanie za pośrednictwem SideKiq / Resque / Celluloid. Potencjalnie w przyszłych wydaniach Railsów API dla Notificationsmoże się zmienić, aby bardziej skupić się na instrumentacji.
Kris
21

Proponuję przeczytać post na blogu Jamesa Golicka pod adresem http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (spróbuj zignorować jak nieskromnie brzmiący tytuł).

Kiedyś był to „gruby model, chudy kontroler”. Wtedy modele tłuszczu stały się gigantycznym bólem głowy, szczególnie podczas testów. Niedawno nacisk padł na chude modele - idea jest taka, że ​​każda klasa powinna odpowiadać za jedną odpowiedzialność, a zadaniem modelu jest utrwalanie danych w bazie danych. Więc gdzie kończy się cała moja złożona logika biznesowa? W klasach logiki biznesowej - klasy reprezentujące transakcje.

Takie podejście może zamienić się w grzęzawisko (giggity), gdy logika zacznie się komplikować. Koncepcja jest jednak rozsądna - zamiast wywoływać rzeczy niejawnie za pomocą wywołań zwrotnych lub obserwatorów, które są trudne do przetestowania i debugowania, wyzwalaj rzeczy jawnie w klasie, która nakłada logikę na model.

MikeJ
źródło
4
Robiłem coś takiego dla projektu przez ostatnie kilka miesięcy. W końcu otrzymujesz wiele małych usług, ale łatwość testowania i utrzymania zdecydowanie przeważa nad wadami. Moje dość obszerne specyfikacje tego średniej wielkości systemu nadal działają tylko 5 sekund :)
Luca Spiller,
Znany również jako PORO (Plain Old Ruby Objects) lub obiekty usługowe
Cyril Duchon-Doris
13

Korzystanie z wywołań zwrotnych aktywnego rekordu po prostu odwraca zależność twojego sprzężenia. Na przykład, jeśli masz modelAi CacheObserverobserwując modelAstyl szyn 3, możesz usunąć CacheObserverbez problemu. Teraz zamiast tego powiedz, że Amusi ręcznie wywołać CacheObserverpo zapisaniu, co byłoby rails 4. Po prostu przeniosłeś swoją zależność, więc możesz bezpiecznie usunąć, Aale nie CacheObserver.

Teraz z mojej wieży z kości słoniowej wolę, aby obserwator był zależny od modelu, który obserwuje. Czy obchodzi mnie na tyle, aby zaśmiecać kontrolery? Dla mnie odpowiedź brzmi: nie.

Przypuszczalnie zastanawiałeś się, dlaczego chcesz / potrzebujesz obserwatora, a zatem stworzenie modelu zależnego od obserwatora nie jest straszną tragedią.

Mam również (jak sądzę rozsądną) wstręt do jakiegokolwiek obserwatora zależnego od działania kontrolera. Nagle musisz wstrzyknąć obserwatorowi dowolną akcję kontrolera (lub inny model), która może zaktualizować model, który chcesz obserwować. Jeśli możesz zagwarantować, że twoja aplikacja będzie modyfikować instancje tylko poprzez tworzenie / aktualizowanie akcji kontrolera, więcej mocy dla ciebie, ale nie jest to założenie, które bym poczynił w odniesieniu do aplikacji railsowej (rozważ zagnieżdżone formularze, modelowanie logiki biznesowej aktualizujących powiązania itp.)

agmin
źródło
1
Dzięki za komentarze @agmin. Z przyjemnością odsuwam się od używania Observera, jeśli istnieje lepszy wzorzec projektowy. Najbardziej interesuje mnie, w jaki sposób inni ludzie budują swój kod i zależności, aby zapewnić podobną funkcjonalność (z wyłączeniem buforowania). W moim przypadku chciałbym rejestrować zmiany w modelu za każdym razem, gdy aktualizowane są jego atrybuty. Kiedyś używałem do tego Observera. Teraz próbuję zdecydować między grubym kontrolerem, wywołaniem zwrotnym AR lub czymś innym, o czym nie pomyślałem. Żadne nie wydaje się w tej chwili eleganckie.
kennyc
13

Wisper to świetne rozwiązanie. Moje osobiste preferencje dotyczące wywołań zwrotnych są takie, że są one uruchamiane przez modele, ale zdarzenia są odsłuchiwane tylko wtedy, gdy przychodzi żądanie, tj. Nie chcę, aby wywołania zwrotne były uruchamiane podczas konfigurowania modeli w testach itp., Ale chcę je uruchamiane za każdym razem, gdy w grę wchodzą kontrolerzy. Jest to naprawdę łatwe do skonfigurowania za pomocą Wispera, ponieważ możesz nakazać mu słuchanie tylko wydarzeń wewnątrz bloku.

class ApplicationController < ActionController::Base
  around_filter :register_event_listeners

  def register_event_listeners(&around_listener_block)
    Wisper.with_listeners(UserListener.new) do
      around_listener_block.call
    end
  end        
end

class User
  include Wisper::Publisher
  after_create{ |user| publish(:user_registered, user) }
end

class UserListener
  def user_registered(user)
    Analytics.track("user:registered", user.analytics)
  end
end
opsb
źródło
9

W niektórych przypadkach po prostu używam Active Support Instrumentation

ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
  # do your stuff here
end

ActiveSupport::Notifications.subscribe "my.custom.event" do |*args|
  data = args.extract_options! # {:this=>:data}
end
Panika
źródło
4

Moją alternatywą dla Rails 3 Observers jest ręczna implementacja, która wykorzystuje wywołanie zwrotne zdefiniowane w modelu, ale udaje się (jak stwierdza agmin w swojej odpowiedzi powyżej) "odwrócić zależność ... sprzężenie".

Moje obiekty dziedziczą z klasy bazowej, która umożliwia rejestrację obserwatorów:

class Party411BaseModel

  self.abstract_class = true
  class_attribute :observers

  def self.add_observer(observer)
    observers << observer
    logger.debug("Observer #{observer.name} added to #{self.name}")
  end

  def notify_observers(obj, event_name, *args)
    observers && observers.each do |observer|
    if observer.respond_to?(event_name)
        begin
          observer.public_send(event_name, obj, *args)
        rescue Exception => e
          logger.error("Error notifying observer #{observer.name}")
          logger.error e.message
          logger.error e.backtrace.join("\n")
        end
    end
  end

end

(To prawda, w duchu kompozycji zamiast dziedziczenia, powyższy kod można umieścić w module i mieszać w każdym modelu).

Inicjator rejestruje obserwatorów:

User.add_observer(NotificationSender)
User.add_observer(ProfilePictureCreator)

Każdy model może następnie zdefiniować własne obserwowalne zdarzenia, poza podstawowymi wywołaniami zwrotnymi ActiveRecord. Na przykład mój model użytkownika ujawnia 2 zdarzenia:

class User < Party411BaseModel

  self.observers ||= []

  after_commit :notify_observers, :on => :create

  def signed_up_via_lunchwalla
    self.account_source == ACCOUNT_SOURCES['LunchWalla']
  end

  def notify_observers
    notify_observers(self, :new_user_created)
    notify_observers(self, :new_lunchwalla_user_created) if self.signed_up_via_lunchwalla
  end
end

Każdy obserwator, który chce otrzymywać powiadomienia o tych zdarzeniach, musi jedynie (1) zarejestrować się w modelu, który ujawnia zdarzenie i (2) mieć metodę, której nazwa odpowiada zdarzeniu. Jak można by się spodziewać, wielu obserwatorów może zarejestrować się na to samo zdarzenie, a (w odniesieniu do drugiego akapitu pierwotnego pytania) obserwator może obserwować zdarzenia w kilku modelach.

Poniższe klasy obserwatorów NotificationSender i ProfilePictureCreator definiują metody dla zdarzeń udostępnianych przez różne modele:

NotificationSender
  def new_user_created(user_id)
    ...
  end

  def new_invitation_created(invitation_id)
    ...
  end

  def new_event_created(event_id)
    ...
  end
end

class ProfilePictureCreator
  def new_lunchwalla_user_created(user_id)
    ...
  end

  def new_twitter_user_created(user_id)
    ...
  end
end

Jedynym zastrzeżeniem jest to, że nazwy wszystkich zdarzeń ujawnionych we wszystkich modelach muszą być niepowtarzalne.

Mark Schneider
źródło
3

Myślę, że problem deprecjonowania Observerów nie polega na tym, że obserwatorzy byli źli sami w sobie, ale to, że byli wykorzystywani.

Przestrzegałbym przed dodawaniem zbyt dużej ilości logiki do wywołań zwrotnych lub po prostu przenoszeniem kodu w celu symulacji zachowania obserwatora, gdy istnieje już rozsądne rozwiązanie tego problemu we wzorcu obserwatora.

Jeśli ma sens korzystanie z obserwatorów, to z całą pewnością używaj obserwatorów. Po prostu zrozum, że będziesz musiał upewnić się, że logika obserwatora jest zgodna z rozsądnymi praktykami kodowania, na przykład SOLID.

Klejnot obserwatora jest dostępny w rubygems, jeśli chcesz go dodać z powrotem do swojego projektu https://github.com/rails/rails-observers

zobacz ten krótki wątek, chociaż nie jest to pełna wyczerpująca dyskusja, myślę, że podstawowy argument jest słuszny. https://github.com/rails/rails-observers/issues/2

hraynaud
źródło
2

A może zamiast tego użyć PORO?

Logika stojąca za tym jest taka, że ​​twoje „dodatkowe akcje przy zapisywaniu” będą prawdopodobnie logiką biznesową. To lubię trzymać oddzielnie od obu modeli AR (które powinny być tak proste, jak to tylko możliwe) i kontrolerów (których testowanie jest kłopotliwe)

class LoggedUpdater

  def self.save!(record)
    record.save!
    #log the change here
  end

end

I po prostu nazwij to tak:

LoggedUpdater.save!(user)

Można go nawet rozszerzyć, wstrzykując dodatkowe obiekty akcji po składowaniu

LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])

I żeby podać przykład „dodatków”. Możesz jednak trochę je podrasować:

class EmailLogger
  def call(msg)
    #send email with msg
  end
end

Jeśli podoba Ci się to podejście, polecam przeczytanie wpisu na blogu Bryana Helmkampsa 7 Patterns .

EDYCJA: Powinienem również wspomnieć, że powyższe rozwiązanie pozwala na dodanie logiki transakcji również w razie potrzeby. Np. Z ActiveRecord i obsługiwaną bazą danych:

class LoggedUpdater

  def self.save!([records])
    ActiveRecord::Base.transaction do
      records.each(&:save!)
      #log the changes here
    end
  end

end
Houen
źródło
-2

Mam ten sam problem! Znajduję rozwiązanie ActiveModel :: Dirty, abyś mógł śledzić zmiany swojego modelu!

include ActiveModel::Dirty
before_save :notify_categories if :data_changed? 


def notify_categories
  self.categories.map!{|c| c.update_results(self.data)}
end

http://api.rubyonrails.org/classes/ActiveModel/Dirty.html

msroot
źródło