Rails Observer Alternatives per 4.0


154

Con gli osservatori ufficialmente rimossi da Rails 4.0 , sono curioso di sapere cosa stanno usando altri sviluppatori al loro posto. (Oltre all'utilizzo della gemma estratta.) Sebbene gli osservatori fossero certamente maltrattati e talvolta diventassero facilmente ingombranti, c'erano molti casi d'uso al di fuori del solo svuotamento della cache in cui erano utili.

Prendi, ad esempio, un'applicazione che deve tenere traccia delle modifiche a un modello. Un osservatore può facilmente controllare le modifiche sul modello A e registrare tali modifiche con il modello B nel database. Se volessi controllare le modifiche su più modelli, un singolo osservatore potrebbe gestirlo.

In Rails 4, sono curioso di sapere quali strategie stanno usando gli altri sviluppatori al posto di Observers per ricreare quella funzionalità.

Personalmente, mi sto orientando verso una sorta di implementazione di "fat controller", in cui queste modifiche sono tracciate nel metodo di creazione / aggiornamento / eliminazione del controller di ciascun modello. Mentre gonfia leggermente il comportamento di ciascun controller, aiuta nella leggibilità e nella comprensione poiché tutto il codice è in un posto. Il rovescio della medaglia è che ora c'è un codice molto simile sparso su diversi controller. L'estrazione di quel codice nei metodi di supporto è un'opzione, ma ti rimangono ancora chiamate a quei metodi disseminati ovunque. Non è la fine del mondo, ma nemmeno nello spirito dei "controllori magri".

I callback di ActiveRecord sono un'altra possibile opzione, sebbene a me personalmente non piaccia perché tende a accoppiare due modelli diversi troppo vicini secondo me.

Quindi nel mondo di Rails 4, no-Observers, se dovessi creare un nuovo record dopo che un altro record è stato creato / aggiornato / distrutto, quale modello di progettazione useresti? Controller Fat, callback ActiveRecord o qualcos'altro interamente?

Grazie.


4
Sono davvero sorpreso che non ci siano più risposte pubblicate per questa domanda. Tipo di sconcertante.
courtimas

Risposte:


82

Dai un'occhiata a Preoccupazioni

Crea una cartella nella directory dei tuoi modelli chiamata preoccupazioni. Aggiungi un modulo lì:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

Successivamente, includi quello nei modelli in cui desideri eseguire after_save:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

A seconda di ciò che stai facendo, questo potrebbe avvicinarti senza osservatori.


20
Ci sono problemi con questo approccio. In particolare, non pulisce i tuoi modelli; include copia i metodi dal modulo alla tua classe. L'estrazione di metodi di classe in un modulo può raggrupparli per preoccupazione, ma la classe è ancora altrettanto gonfia.
Steven Soroka,

15
Il titolo è "Rails Observer Alternatives for 4.0" e non "Come posso ridurre al minimo il gonfiamento". Com'è che le preoccupazioni non fanno il lavoro Steven? E no, suggerire che 'gonfiare' è una ragione per cui questo non funzionerà come sostituto degli osservatori non è abbastanza buono. Dovrai trovare un suggerimento migliore per aiutare la comunità o spiegare perché le preoccupazioni non funzioneranno in sostituzione degli osservatori. Spero che dichiarerai entrambi = D
UncleAdam il

10
Bloat è sempre una preoccupazione. Un'alternativa migliore è Wisper , che, se implementato correttamente, consente di eliminare i problemi estraendoli in classi separate che non sono strettamente accoppiate ai modelli. Questo rende anche molto più semplice il test in isolamento
Steven Soroka,

4
Gonfia modello o Gonfia intera app tirando una gemma per farlo: possiamo lasciarlo alle preferenze individuali. Grazie per il suggerimento aggiuntivo.
ZioAdam,

Sarebbe solo gonfiare il menu di completamento automatico del metodo IDE, che dovrebbe andare bene per molte persone.
Lulalala,

33

Ora sono in un plugin .

Posso anche raccomandare un'alternativa che ti darà controller come:

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

Che ne dici di ActiveSupport :: Notifiche?
svoop

Gli @svoop ActiveSupport::Notificationssono orientati alla strumentazione, non a un sub / pub generico.
Kris

@Kris - hai ragione. Viene utilizzato principalmente per la strumentazione, ma mi chiedo cosa ne impedisca l'utilizzo come metodo generico per pub / sub? fornisce i mattoni di base, giusto? In altre parole, quali sono gli aspetti positivi / negativi da migliorare rispetto a ActiveSupport::Notifications?
Gingerlime,

Non ho usato Notificationsmolto, ma direi che Wisperha un'API più bella e funzionalità come "abbonati globali", "su prefisso" e "mappatura degli eventi" che Notificationsnon lo fanno. Una versione futura di Wisperconsentirà anche la pubblicazione asincrona tramite SideKiq / Resque / Celluloid. Inoltre, potenzialmente, nelle future versioni di Rails, l'API per Notificationspotrebbe cambiare per essere più focalizzata sulla strumentazione.
Kris,

21

Il mio consiglio è di leggere il post sul blog di James Golick su http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (prova a ignorare come sembra immodesto il titolo).

Ai tempi era tutto "modello grasso, controller magro". Quindi i modelli di grasso sono diventati un grosso mal di testa, specialmente durante i test. Più recentemente la spinta è stata per i modelli skinny - l'idea è che ogni classe dovrebbe gestire una responsabilità e il lavoro di un modello è quello di conservare i dati in un database. Quindi dove finisce tutta la mia complessa logica aziendale? Nelle classi di business logic: classi che rappresentano le transazioni.

Questo approccio può trasformarsi in un pantano (risatina) quando la logica inizia a complicarsi. Il concetto è valido, invece di innescare le cose in modo implicito con callback o osservatori difficili da testare ed eseguire il debug, innescando le cose in modo esplicito in una classe che sovrappone la logica al modello.


4
Ho fatto qualcosa del genere per un progetto negli ultimi mesi. Si finisce con un sacco di piccoli servizi, ma la facilità di test e manutenzione supera sicuramente gli svantaggi. Le mie specifiche piuttosto estese su questo sistema di medie dimensioni richiedono ancora solo 5 secondi per funzionare :)
Luca Spiller

Conosciuto anche come PORO (Plain Old Ruby Objects) o oggetti di servizio
Cyril Duchon-Doris,

13

L'uso di callback di record attivi ribalta semplicemente la dipendenza dell'accoppiamento. Ad esempio, se disponi di modelAuno stile di binari di CacheObserverosservazione modelA3, puoi rimuoverlo CacheObserversenza problemi. Ora, invece, dire che Adeve invocare manualmente il CacheObserverdopo salvataggio, che sarebbe rotaie 4. Hai semplicemente spostato la tua dipendenza in modo da poterlo rimuovere in sicurezza Ama non CacheObserver.

Ora, dalla mia torre d'avorio preferisco che l'osservatore dipenda dal modello che sta osservando. Mi interessa abbastanza per ingombrare i miei controller? Per me la risposta è no.

Presumibilmente hai riflettuto sul motivo per cui desideri / hai bisogno dell'osservatore, e quindi creare un modello dipendente dal suo osservatore non è una terribile tragedia.

Ho anche un disgusto (ragionevolmente fondato, penso) per qualsiasi tipo di osservatore dipendente da un'azione del controller. Improvvisamente devi iniettare il tuo osservatore in qualsiasi azione del controller (o un altro modello) che possa aggiornare il modello che desideri osservare. Se puoi garantire che la tua app modificherà le istanze solo tramite le azioni di creazione / aggiornamento del controller, maggiore potenza per te, ma non è un presupposto che farei su un'applicazione rails (considera i moduli nidificati, modella le associazioni di aggiornamento della logica aziendale, ecc.)


1
Grazie per i commenti @agmin. Sono felice di abbandonare l'uso di un osservatore se esiste un modello di progettazione migliore là fuori. Sono molto interessato a come le altre persone stanno strutturando il loro codice e le dipendenze per fornire funzionalità simili (esclusa la cache). Nel mio caso, vorrei registrare le modifiche a un modello ogni volta che i suoi attributi vengono aggiornati. Per farlo usavo un osservatore. Ora sto cercando di decidere tra un controller fat, callback AR o qualcos'altro a cui non avevo pensato. Nessuno dei due sembra elegante al momento.
Kennedy

13

Wisper è un'ottima soluzione. La mia preferenza personale per i callback è che vengono attivati ​​dai modelli ma gli eventi vengono ascoltati solo quando arriva una richiesta, cioè non voglio che i callback vengano attivati ​​mentre sto impostando i modelli nei test ecc., Ma li voglio licenziato ogni volta che sono coinvolti controller. Questo è davvero facile da configurare con Wisper perché puoi dire di ascoltare solo eventi all'interno di un blocco.

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

9

In alcuni casi uso semplicemente la strumentazione di supporto attiva

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

4

La mia alternativa a Rails 3 Observers è un'implementazione manuale che utilizza un callback definito all'interno del modello ma riesce (come afferma agmin nella sua risposta sopra) a "capovolgere la dipendenza ... accoppiamento".

I miei oggetti ereditano da una classe base che prevede la registrazione degli osservatori:

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

(Concesso, nello spirito della composizione rispetto all'eredità, il codice sopra potrebbe essere inserito in un modulo e miscelato in ciascun modello.)

Un inizializzatore registra gli osservatori:

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

Ciascun modello può quindi definire i propri eventi osservabili, oltre ai callback di base di ActiveRecord. Ad esempio, il mio modello utente espone 2 eventi:

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

Ogni osservatore che desidera ricevere notifiche per quegli eventi deve semplicemente (1) registrarsi con il modello che espone l'evento e (2) avere un metodo il cui nome corrisponde all'evento. Come ci si potrebbe aspettare, più osservatori possono registrarsi per lo stesso evento e (in riferimento al secondo paragrafo della domanda originale) un osservatore può guardare gli eventi su più modelli.

Le seguenti classi di osservatori NotificationSender e ProfilePictureCreator definiscono i metodi per gli eventi esposti da vari modelli:

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

Un avvertimento è che i nomi di tutti gli eventi esposti su tutti i modelli devono essere univoci.


3

Penso che il problema con gli osservatori che sono deprecati non sia che gli osservatori fossero cattivi in ​​sé e per sé, ma che fossero stati abusati.

Vorrei mettere in guardia dall'aggiunta di troppa logica nei callback o dal semplice spostamento del codice per simulare il comportamento di un osservatore quando esiste già una valida soluzione a questo problema nel modello Observer.

Se ha senso usare osservatori, allora usa osservatori. Devi solo capire che dovrai assicurarti che la tua logica di osservatore segua le pratiche di codifica del suono, ad esempio SOLID.

La gemma dell'osservatore è disponibile su rubygems se vuoi aggiungerla di nuovo al tuo progetto https://github.com/rails/rails-observers

vedere questo breve thread, sebbene non sia una discussione completa e completa, penso che l'argomento di base sia valido. https://github.com/rails/rails-observers/issues/2



2

Che ne dici di usare un PORO invece?

La logica alla base di ciò è che le tue "azioni extra sul salvataggio" saranno probabilmente logiche aziendali. Mi piace tenermi separato da entrambi i modelli AR (che dovrebbero essere il più semplici possibile) e dai controller (che sono fastidiosi da testare correttamente)

class LoggedUpdater

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

end

E semplicemente chiamalo come tale:

LoggedUpdater.save!(user)

Potresti persino espanderci, iniettando ulteriori oggetti azione post-salvataggio

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

E per fare un esempio degli "extra". Potresti volerli spifferare un po 'però:

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

Se ti piace questo approccio, ti consiglio di leggere il post sul blog di Bryan Helmkamps 7 Patterns .

EDIT: dovrei anche menzionare che la soluzione di cui sopra consente di aggiungere anche la logica delle transazioni quando necessario. Ad esempio con ActiveRecord e un database supportato:

class LoggedUpdater

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

end


-2

Ho lo stesso probjem! Trovo una soluzione ActiveModel :: Dirty in modo da poter tenere traccia delle modifiche del modello!

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.