Rails Observer-Alternativen für 4.0

153

Da Observers offiziell aus Rails 4.0 entfernt wurden , bin ich gespannt, was andere Entwickler an ihrer Stelle verwenden. (Abgesehen von der Verwendung des extrahierten Edelsteins.) Während Beobachter sicherlich missbraucht wurden und manchmal leicht unhandlich werden konnten, gab es viele Anwendungsfälle außerhalb des Cache-Löschens, bei denen sie von Vorteil waren.

Nehmen Sie zum Beispiel eine Anwendung, die Änderungen an einem Modell verfolgen muss. Ein Beobachter kann problemlos nach Änderungen an Modell A suchen und diese Änderungen mit Modell B in der Datenbank aufzeichnen. Wenn Sie auf Änderungen in mehreren Modellen achten möchten, kann ein einzelner Beobachter damit umgehen.

In Rails 4 bin ich gespannt, welche Strategien andere Entwickler anstelle von Beobachtern verwenden, um diese Funktionalität wiederherzustellen.

Persönlich neige ich zu einer Art "Fat Controller" -Implementierung, bei der diese Änderungen in der Erstellungs- / Aktualisierungs- / Löschmethode jedes Controller-Modells verfolgt werden. Während es das Verhalten jedes Controllers leicht aufbläht, hilft es bei der Lesbarkeit und dem Verständnis, da sich der gesamte Code an einem Ort befindet. Der Nachteil ist, dass es jetzt Code gibt, der sehr ähnlich ist und auf mehrere Controller verteilt ist. Das Extrahieren dieses Codes in Hilfsmethoden ist eine Option, aber Sie haben immer noch Aufrufe für diese Methoden, die überall verstreut sind. Nicht das Ende der Welt, aber auch nicht ganz im Sinne von "Skinny Controllern".

ActiveRecord-Rückrufe sind eine weitere mögliche Option, die ich persönlich nicht mag, da sie meiner Meinung nach dazu neigen, zwei verschiedene Modelle zu eng miteinander zu verbinden.

Wenn Sie in der Rails 4-Welt ohne Beobachter einen neuen Datensatz erstellen müssten, nachdem ein anderer Datensatz erstellt / aktualisiert / zerstört wurde, welches Entwurfsmuster würden Sie verwenden? Fat Controller, ActiveRecord-Rückrufe oder etwas ganz anderes?

Danke dir.

kennyc
quelle
4
Ich bin wirklich überrascht, dass auf diese Frage keine weiteren Antworten veröffentlicht wurden. Art beunruhigend.
Courtsimas

Antworten:

82

Werfen Sie einen Blick auf Bedenken

Erstellen Sie in Ihrem Modellverzeichnis einen Ordner mit dem Namen "Anliegen". Fügen Sie dort ein Modul hinzu:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Fügen Sie als Nächstes Folgendes in die Modelle ein, in denen Sie after_save ausführen möchten:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Je nachdem, was Sie tun, können Sie sich ohne Beobachter nähern.

Onkel Adam
quelle
20
Es gibt Probleme mit diesem Ansatz. Insbesondere werden Ihre Modelle nicht aufgeräumt. include kopiert die Methoden aus dem Modul zurück in Ihre Klasse. Das Extrahieren von Klassenmethoden in ein Modul kann sie nach Bedenken gruppieren, aber die Klasse ist immer noch genauso aufgebläht.
Steven Soroka
15
Der Titel lautet "Rails Observer Alternatives for 4.0" und nicht "Wie minimiere ich das Aufblähen?". Wie kommt es, dass Bedenken den Job Steven nicht machen? Und nein, es ist nicht gut genug zu behaupten, dass "Aufblähen" ein Grund ist, warum dies nicht als Ersatz für Beobachter funktioniert. Sie müssen sich einen besseren Vorschlag einfallen lassen, um der Community zu helfen oder zu erklären, warum Bedenken nicht als Ersatz für Beobachter dienen. Hoffentlich geben Sie beide an = D
UncleAdam
10
Aufblähen ist immer ein Problem. Eine bessere Alternative ist Wisper , mit dem Sie bei ordnungsgemäßer Implementierung die Probleme bereinigen können, indem Sie sie in separate Klassen extrahieren, die nicht eng mit den Modellen verbunden sind. Dies macht es auch viel einfacher, isoliert zu testen
Steven Soroka
4
Model Bloat oder Whole App Bloat durch Ziehen eines Edelsteins, um dies zu tun - wir können es den individuellen Vorlieben überlassen. Danke für den zusätzlichen Vorschlag.
Onkel Adam
Es würde nur das automatische Vervollständigungsmenü der IDE-Methode aufblähen, was für viele Menschen in Ordnung sein sollte.
Lulalala
33

Sie sind jetzt in einem Plugin .

Kann ich auch eine Alternative empfehlen, die Ihnen Controller bietet wie:

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
quelle
Wie wäre es mit ActiveSupport :: Notifications?
Svoop
@svoop ActiveSupport::Notificationssind auf Instrumentierung ausgerichtet, nicht auf generisches Sub / Pub.
Kris
@Kris - du hast recht. Es wird hauptsächlich für die Instrumentierung verwendet, aber ich frage mich, was verhindert, dass es als generische Methode für Pub / Sub verwendet wird. es liefert die Grundbausteine, richtig? Mit anderen Worten, was sind die Vor- und Nachteile, mit denen man flüstern muss ActiveSupport::Notifications?
Ingwerlime
Ich habe nicht Notificationsviel verwendet , aber ich würde sagen, dass Wisperes eine schönere API und Funktionen wie "globale Abonnenten", "Präfix" und "Ereigniszuordnung" gibt, die Notificationsdies nicht tun. Eine zukünftige Version von Wisperwird auch das asynchrone Veröffentlichen über SideKiq / Resque / Celluloid ermöglichen. Möglicherweise wird sich die API für zukünftige Rails-Versionen Notificationsändern, um sich stärker auf die Instrumentierung zu konzentrieren.
Kris
21

Mein Vorschlag ist, James Golicks Blog-Beitrag unter http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html zu lesen (versuchen Sie zu ignorieren, wie unbescheiden klingt der Titel).

Früher war alles "fettes Modell, dünner Controller". Dann wurden die fetten Modelle zu riesigen Kopfschmerzen, besonders beim Testen. In jüngerer Zeit wurden dünne Modelle vorangetrieben. Die Idee war, dass jede Klasse eine Verantwortung übernehmen sollte und die Aufgabe eines Modells darin besteht, Ihre Daten in einer Datenbank zu speichern. Wo also landet all meine komplexe Geschäftslogik? In Geschäftslogikklassen - Klassen, die Transaktionen darstellen.

Dieser Ansatz kann sich in einen Sumpf (Giggity) verwandeln, wenn die Logik kompliziert wird. Das Konzept ist jedoch solide - anstatt Dinge implizit mit Rückrufen oder Beobachtern auszulösen, die schwer zu testen und zu debuggen sind, lösen Sie Dinge explizit in einer Klasse aus, die Logik über Ihr Modell legt.

MikeJ
quelle
4
Ich habe in den letzten Monaten so etwas für ein Projekt gemacht. Sie haben zwar viele kleine Dienste, aber die einfache Prüfung und Wartung überwiegt definitiv die Nachteile. Meine ziemlich umfangreichen Spezifikationen für dieses mittelgroße System dauern immer noch nur 5 Sekunden :)
Luca Spiller
Auch bekannt als PORO (Plain Old Ruby Objects) oder Service-Objekte
Cyril Duchon-Doris
13

Durch die Verwendung aktiver Datensatzrückrufe wird einfach die Abhängigkeit Ihrer Kopplung umgedreht. Wenn Sie beispielsweise modelAeinen CacheObserverBeobachtungsschienen- modelA3-Stil haben, können Sie diesen problemlos entfernen CacheObserver. Jetzt muss say stattdessen Amanuell das CacheObserverAfter-Save aufrufen , was Rails 4 wäre. Sie haben einfach Ihre Abhängigkeit verschoben, damit Sie sie sicher entfernen können, Aaber nicht CacheObserver.

Von meinem Elfenbeinturm aus bevorzuge ich, dass der Beobachter von dem Modell abhängig ist, das er beobachtet. Interessiert es mich genug, meine Controller zu überladen? Für mich lautet die Antwort nein.

Vermutlich haben Sie sich Gedanken darüber gemacht, warum Sie den Beobachter wollen / brauchen, und daher ist es keine schreckliche Tragödie, ein Modell zu erstellen, das von seinem Beobachter abhängig ist.

Ich habe auch eine (meiner Meinung nach begründete) Abneigung gegen jede Art von Beobachter, der von einer Controller-Aktion abhängig ist. Plötzlich müssen Sie Ihren Beobachter in eine Controller-Aktion (oder ein anderes Modell) einbinden, die möglicherweise das Modell aktualisiert, das Sie beobachten möchten. Wenn Sie garantieren können, dass Ihre App Instanzen immer nur über Aktionen zum Erstellen / Aktualisieren von Controllern ändert, haben Sie mehr Leistung, aber das ist keine Annahme, die ich über eine Rails-Anwendung machen würde (berücksichtigen Sie verschachtelte Formulare, Modellzuordnungen zur Aktualisierung der Geschäftslogik usw.).

agmin
quelle
1
Danke für die Kommentare @agmin. Ich bin froh, keinen Observer mehr zu verwenden, wenn es da draußen ein besseres Designmuster gibt. Ich bin am meisten daran interessiert, wie andere Leute ihren Code und ihre Abhängigkeiten strukturieren, um ähnliche Funktionen bereitzustellen (ohne Caching). In meinem Fall möchte ich die Änderungen an einem Modell jedes Mal aufzeichnen, wenn seine Attribute aktualisiert werden. Ich habe dazu einen Observer benutzt. Jetzt versuche ich mich zwischen einem Fat Controller, einem AR-Rückruf oder etwas anderem zu entscheiden, an das ich nicht gedacht hatte. Beides scheint im Moment nicht elegant zu sein.
Kennyc
13

Wisper ist eine großartige Lösung. Meine persönliche Präferenz für Rückrufe ist, dass sie von den Modellen ausgelöst werden, die Ereignisse jedoch nur abgehört werden, wenn eine Anfrage eingeht, dh ich möchte nicht, dass Rückrufe ausgelöst werden, während ich Modelle in Tests usw. einrichte, aber ich möchte sie Wird ausgelöst, wenn Controller beteiligt sind. Dies ist mit Wisper sehr einfach einzurichten, da Sie festlegen können, dass nur Ereignisse innerhalb eines Blocks abgehört werden sollen.

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
quelle
9

In einigen Fällen verwende ich einfach 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
Panik
quelle
4

Meine Alternative zu Rails 3 Observers ist eine manuelle Implementierung, die einen im Modell definierten Rückruf verwendet und es dennoch schafft (wie in seiner obigen Antwort angegeben), die Abhängigkeit ... Kopplung umzudrehen.

Meine Objekte erben von einer Basisklasse, die die Registrierung von Beobachtern ermöglicht:

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

(Zugegeben, im Sinne der Komposition über die Vererbung könnte der obige Code in ein Modul eingefügt und in jedes Modell gemischt werden.)

Ein Initialisierer registriert Beobachter:

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

Jedes Modell kann dann über die grundlegenden ActiveRecord-Rückrufe hinaus seine eigenen beobachtbaren Ereignisse definieren. Zum Beispiel macht mein Benutzermodell zwei Ereignisse verfügbar:

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

Jeder Beobachter, der Benachrichtigungen für diese Ereignisse erhalten möchte, muss sich lediglich (1) bei dem Modell registrieren, das das Ereignis verfügbar macht, und (2) über eine Methode verfügen, deren Name mit dem Ereignis übereinstimmt. Wie zu erwarten ist, können sich mehrere Beobachter für dasselbe Ereignis registrieren, und (in Bezug auf den zweiten Absatz der ursprünglichen Frage) ein Beobachter kann über mehrere Modelle hinweg nach Ereignissen suchen.

Die folgenden Beobachterklassen NotificationSender und ProfilePictureCreator definieren Methoden für die Ereignisse, die von verschiedenen Modellen angezeigt werden:

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

Eine Einschränkung ist, dass die Namen aller Ereignisse, die in allen Modellen angezeigt werden, eindeutig sein müssen.

Mark Schneider
quelle
3

Ich denke, das Problem mit der Ablehnung von Beobachtern ist nicht, dass Beobachter an und für sich schlecht waren, sondern dass sie missbraucht wurden.

Ich würde davor warnen, Ihren Rückrufen zu viel Logik hinzuzufügen oder einfach Code zu verschieben, um das Verhalten eines Beobachters zu simulieren, wenn es bereits eine fundierte Lösung für dieses Problem gibt, das Observer-Muster.

Wenn es sinnvoll ist, Beobachter einzusetzen, sollten Sie auf jeden Fall Beobachter einsetzen. Verstehen Sie einfach, dass Sie sicherstellen müssen, dass Ihre Beobachterlogik den Soundcodierungspraktiken folgt, z. B. SOLID.

Das Observer Gem ist auf Rubygems verfügbar, wenn Sie es wieder zu Ihrem Projekt hinzufügen möchten: https://github.com/rails/rails-observers

siehe diesen kurzen Thread, obwohl nicht vollständige Diskussion Ich denke, das grundlegende Argument ist gültig. https://github.com/rails/rails-observers/issues/2

hraynaud
quelle
2

Wie wäre es stattdessen mit einem PORO?

Die Logik dahinter ist, dass Ihre "zusätzlichen Aktionen beim Speichern" wahrscheinlich Geschäftslogik sein werden. Dies möchte ich sowohl von AR-Modellen (die so einfach wie möglich sein sollten) als auch von Controllern (die lästig sind, um richtig zu testen) getrennt halten.

class LoggedUpdater

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

end

Und nennen Sie es einfach so:

LoggedUpdater.save!(user)

Sie können es sogar erweitern, indem Sie zusätzliche Aktionsobjekte nach dem Speichern einfügen

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

Und um ein Beispiel für die "Extras" zu geben. Vielleicht möchten Sie sie aber ein bisschen aufpeppen:

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

Wenn Ihnen dieser Ansatz gefällt, empfehle ich Ihnen, den Blog-Beitrag von Bryan Helmkamps 7 Patterns zu lesen .

EDIT: Ich sollte auch erwähnen, dass die obige Lösung das Hinzufügen von Transaktionslogik bei Bedarf auch ermöglicht. ZB mit ActiveRecord und einer unterstützten Datenbank:

class LoggedUpdater

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

end
Houen
quelle
-2

Ich habe das gleiche Problem! Ich finde eine Lösung ActiveModel :: Dirty, damit Sie Ihre Modelländerungen verfolgen können!

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
quelle