Alternativas del observador de rieles para 4.0

154

Con los observadores eliminados oficialmente de Rails 4.0 , tengo curiosidad por saber qué otros desarrolladores están usando en su lugar. (Aparte de usar la gema extraída.) Si bien los observadores ciertamente fueron abusados ​​y fácilmente podrían volverse difíciles de manejar a veces, hubo muchos casos de uso fuera del solo borrado de caché donde fueron beneficiosos.

Tomemos, por ejemplo, una aplicación que necesita rastrear cambios en un modelo. Un observador podría observar fácilmente los cambios en el Modelo A y registrar esos cambios con el Modelo B en la base de datos. Si desea observar los cambios en varios modelos, entonces un solo observador podría manejar eso.

En Rails 4, tengo curiosidad por saber qué estrategias están usando otros desarrolladores en lugar de Observers para recrear esa funcionalidad.

Personalmente, me estoy inclinando hacia una especie de implementación de "controlador gordo", donde estos cambios se rastrean en el método de creación / actualización / eliminación del controlador de cada modelo. Si bien aumenta ligeramente el comportamiento de cada controlador, ayuda en la legibilidad y la comprensión, ya que todo el código está en un solo lugar. La desventaja es que ahora hay un código muy similar disperso en varios controladores. Extraer ese código en los métodos auxiliares es una opción, pero aún le quedan llamadas a esos métodos en todas partes. No es el fin del mundo, pero tampoco en el espíritu de "controladores flacos".

Las devoluciones de llamada de ActiveRecord son otra opción posible, aunque personalmente no me gusta, ya que, en mi opinión, tiende a acoplar dos modelos diferentes.

Entonces, en el mundo de Rails 4, sin observadores, si tuviera que crear un nuevo registro después de que otro registro fue creado / actualizado / destruido, ¿qué patrón de diseño usaría? ¿Controladores gordos, devoluciones de llamada ActiveRecord o algo completamente diferente?

Gracias.

kennyc
fuente
44
Estoy realmente sorprendido de que no haya más respuestas publicadas para esta pregunta. Un poco desconcertante.
courtimas

Respuestas:

82

Echa un vistazo a las preocupaciones

Cree una carpeta en su directorio de modelos llamada preocupaciones. Agregue un módulo allí:

module MyConcernModule
  extend ActiveSupport::Concern

  included do
    after_save :do_something
  end

  def do_something
     ...
  end
end

A continuación, inclúyalo en los modelos en los que desea ejecutar after_save en:

class MyModel < ActiveRecord::Base
  include MyConcernModule
end

Dependiendo de lo que esté haciendo, esto podría acercarlo sin observadores.

TíoAdam
fuente
20
Hay problemas con este enfoque. Notablemente, no limpia sus modelos; incluir copias de los métodos del módulo de nuevo en su clase. Extraer los métodos de clase a un módulo puede agruparlos por preocupación, pero la clase sigue igual de hinchada.
Steven Soroka
15
El título es 'Alternativas del observador de rieles para 4.0' y no 'Cómo minimizo la hinchazón'. ¿Cómo es que las preocupaciones no hacen el trabajo Steven? Y no, sugerir que "hinchazón" es una razón por la cual esto no funcionará como un reemplazo para los observadores no es lo suficientemente bueno. Tendrá que proponer una mejor sugerencia para ayudar a la comunidad o explicar por qué las preocupaciones no funcionarán como un reemplazo para los observadores. Espero que
digas
10
La hinchazón es siempre una preocupación. Una mejor alternativa es wisper , que, si se implementa adecuadamente, le permite solucionar los problemas extrayéndolos a clases separadas que no están estrechamente relacionadas con los modelos. Esto también hace que sea mucho más fácil probar en forma aislada
Steven Soroka,
44
Modele la hinchazón o la aplicación completa aplicando una Gema para hacer esto; podemos dejarlo a la preferencia individual. Gracias por la sugerencia adicional.
TíoAdam
Solo aumentaría el menú de autocompletado del método IDE, lo que debería estar bien para muchas personas.
lulalala
33

Están en un complemento ahora.

¿Puedo recomendar también una alternativa que le dará controladores como:

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
fuente
¿Qué hay de ActiveSupport :: Notificaciones?
svoop
@svoop ActiveSupport::Notificationsestán orientados a la instrumentación, no a los sub / pub genéricos.
Kris
@Kris: tienes razón. Se usa principalmente para instrumentación, pero me pregunto qué impide que se use como método genérico para pub / sub. proporciona los bloques de construcción básicos, ¿verdad? En otras palabras, ¿cuáles son las ventajas y desventajas de wisper en comparación ActiveSupport::Notifications?
gingerlime
No he usado Notificationsmucho, pero diría que Wispertiene una API más agradable y características como 'suscriptores globales', 'en prefijo' y 'mapeo de eventos' que Notificationsno lo hace. Una versión futura de Wispertambién permitirá la publicación asincrónica a través de SideKiq / Resque / Celluloid. Además, potencialmente, en futuras versiones de Rails, la API Notificationspodría cambiar para centrarse más en la instrumentación.
Kris
21

Mi sugerencia es leer la publicación del blog de James Golick en http://jamesgolick.com/2010/3/14/crazy-heretical-and-awesome-the-way-i-write-rails-apps.html (trate de ignorar cómo inmodesto suena el título).

En el pasado todo era "modelo gordo, controlador flaco". Luego, los modelos gordos se convirtieron en un dolor de cabeza gigante, especialmente durante las pruebas. Más recientemente, el impulso ha sido para modelos flacos: la idea es que cada clase debe manejar una responsabilidad y el trabajo de un modelo es mantener sus datos en una base de datos. Entonces, ¿dónde termina toda mi lógica empresarial compleja? En las clases de lógica de negocios: clases que representan transacciones.

Este enfoque puede convertirse en un atolladero (risas) cuando la lógica comienza a complicarse. Sin embargo, el concepto es sólido: en lugar de desencadenar cosas implícitamente con devoluciones de llamada u observadores que son difíciles de probar y depurar, desencadenar cosas explícitamente en una clase que superponga la lógica sobre su modelo.

MikeJ
fuente
44
He estado haciendo algo así para un proyecto en los últimos meses. Terminas con muchos pequeños servicios, pero la facilidad de probarlo y mantenerlo definitivamente supera las desventajas. Mis especificaciones bastante extensas en este sistema de tamaño mediano todavía solo tardan 5 segundos en ejecutarse :)
Luca Spiller
También conocido como PORO (Plain Old Ruby Objects), u objetos de servicio
Cyril Duchon-Doris
13

El uso de devoluciones de llamada de registro activas simplemente cambia la dependencia de su acoplamiento. Por ejemplo, si tiene modelAun estilo de rieles de CacheObserverobservación modelA3, puede eliminarlo CacheObserversin ningún problema. Ahora, en su lugar, digamos que Atiene que invocar manualmente el CacheObserverguardado posterior, que serían rieles 4. Simplemente ha movido su dependencia para que pueda eliminarla con seguridad Apero no CacheObserver.

Ahora, desde mi torre de marfil, prefiero que el observador dependa del modelo que está observando. ¿Me importa lo suficiente como para desordenar mis controladores? Para mi, la respuesta es no.

Presumiblemente has pensado en por qué quieres / necesitas al observador, y así crear un modelo que dependa de su observador no es una tragedia terrible.

También tengo un disgusto (razonablemente fundamentado, creo) por cualquier tipo de observador que dependa de una acción del controlador. De repente, debe inyectar a su observador en cualquier acción del controlador (u otro modelo) que pueda actualizar el modelo que desea observar. Si puede garantizar que su aplicación solo modificará instancias a través de acciones de controlador de creación / actualización, más poder para usted, pero eso no es una suposición que haría sobre una aplicación de rieles (considere formas anidadas, asociaciones de actualización de lógica de negocios modelo, etc.)

agmin
fuente
1
Gracias por los comentarios @agmin. Estoy feliz de dejar de usar un Observador si hay un mejor patrón de diseño. Estoy más interesado en cómo otras personas están estructurando su código y sus dependencias para proporcionar una funcionalidad similar (excluyendo el almacenamiento en caché). En mi caso, me gustaría registrar los cambios en un modelo cada vez que se actualizan sus atributos. Solía ​​usar un observador para hacer eso. Ahora estoy tratando de decidir entre un controlador gordo, una devolución de llamada AR o algo más en lo que no había pensado. Ninguno de los dos parece elegante en este momento.
kennyc
13

Wisper es una gran solución. Mi preferencia personal para las devoluciones de llamada es que son activadas por los modelos, pero los eventos solo se escuchan cuando llega una solicitud, es decir, no quiero que se activen las devoluciones de llamada mientras configuro modelos en pruebas, etc. pero sí los quiero despedido cuando los controladores están involucrados. Esto es realmente fácil de configurar con Wisper porque puedes decirle que solo escuche eventos dentro de un bloque.

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

En algunos casos, simplemente uso la Instrumentación de soporte activo

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
Pánico
fuente
4

Mi alternativa a Rails 3 Observers es una implementación manual que utiliza una devolución de llamada definida dentro del modelo pero que (como afirma Agmin en su respuesta anterior) "cambia la dependencia ... acoplamiento".

Mis objetos heredan de una clase base que permite registrar observadores:

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

(Por supuesto, en el espíritu de composición sobre la herencia, el código anterior podría colocarse en un módulo y mezclarse en cada modelo).

Un inicializador registra observadores:

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

Cada modelo puede definir sus propios eventos observables, más allá de las devoluciones de llamada básicas de ActiveRecord. Por ejemplo, mi modelo de usuario expone 2 eventos:

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

Cualquier observador que desee recibir notificaciones para esos eventos simplemente necesita (1) registrarse con el modelo que expone el evento y (2) tener un método cuyo nombre coincida con el evento. Como cabría esperar, múltiples observadores pueden registrarse para el mismo evento y (en referencia al segundo párrafo de la pregunta original) un observador puede observar eventos en varios modelos.

Las clases de observador NotificationSender y ProfilePictureCreator a continuación definen métodos para los eventos expuestos por varios modelos:

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

Una advertencia es que los nombres de todos los eventos expuestos en todos los modelos deben ser únicos.

Mark Schneider
fuente
3

Creo que el problema con la desaprobación de los observadores no es que los observadores fueran malos en sí mismos, sino que estaban siendo abusados.

Yo advertiría de no agregar demasiada lógica en sus devoluciones de llamada o simplemente mover el código para simular el comportamiento de un observador cuando ya hay una solución sólida a este problema, el patrón Observador.

Si tiene sentido usar observadores, entonces, por supuesto, use observadores. Solo comprenda que deberá asegurarse de que la lógica de su observador siga las prácticas de codificación de sonido, por ejemplo, SOLID.

La gema del observador está disponible en rubygems si desea agregarla nuevamente a su proyecto https://github.com/rails/rails-observers

vea este breve hilo, aunque no es una discusión exhaustiva completa, creo que el argumento básico es válido. https://github.com/rails/rails-observers/issues/2

hraynaud
fuente
2

¿Qué tal usar un PORO en su lugar?

La lógica detrás de esto es que sus 'acciones adicionales en guardar' probablemente serán lógica de negocios. Esto me gusta mantenerme separado de los modelos AR (que deberían ser lo más simples posible) y los controladores (que son molestos de probar correctamente)

class LoggedUpdater

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

end

Y simplemente llámalo como tal:

LoggedUpdater.save!(user)

Incluso podría ampliarlo inyectando objetos de acción adicionales posteriores al guardado

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

Y para dar un ejemplo de los 'extras'. Sin embargo, es posible que desee ampliarlos un poco:

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

Si le gusta este enfoque, le recomiendo una lectura de la publicación del blog Bryan Helmkamps 7 Patterns .

EDITAR: También debo mencionar que la solución anterior también permite agregar lógica de transacción cuando sea necesario. Por ejemplo, con ActiveRecord y una base de datos compatible:

class LoggedUpdater

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

end
Houen
fuente
-2

Tengo el mismo problema! ¡Encuentro una solución ActiveModel :: Dirty para que pueda seguir los cambios de su modelo!

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
fuente