ruby-on-rails - rubyonrails - ruby on rails tutorial
Rails Observer Alternatives for 4.0 (12)
¡Tengo el mismo probjem! ¡Encontré 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
Con los observadores oficialmente eliminados de Rails 4.0 , tengo curiosidad por saber qué están usando otros desarrolladores en su lugar. (Aparte de usar la gema extraída). Aunque los observadores eran sin duda objeto de abuso y podían volverse fácilmente difíciles de manejar a veces, había muchos casos de uso fuera de la limpieza del caché donde eran beneficiosos.
Tome, por ejemplo, una aplicación que necesita rastrear cambios a 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, un solo observador podría manejar eso.
En Rails 4, tengo curiosidad por las estrategias que usan otros desarrolladores en lugar de los observadores para recrear esa funcionalidad.
Personalmente, me inclino por una especie de implementación de "controlador de grasa", donde estos cambios se rastrean en el método de creación / actualización / eliminación de cada controlador de modelos. Si bien aumenta el comportamiento de cada controlador ligeramente, ayuda a la lectura y la comprensión ya que todo el código está en un solo lugar. La desventaja es que ahora hay un código que es muy similar disperso en varios controladores. La extracción de ese código en 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 está en el espíritu de "controladores delgados".
Las devoluciones de llamada de ActiveRecord son otra opción posible, aunque una que no me gusta personalmente ya que tiende a unir dos modelos diferentes muy estrechamente en mi opinión.
Entonces, en el mundo de Rails 4, no-observadores, si tuvieras que crear un nuevo registro después de crear / actualizar / destruir otro, ¿qué patrón de diseño utilizarías? ¿Controladores de grasa, devoluciones de llamada de ActiveRecord, o algo completamente distinto?
Gracias.
¿Qué hay de usar un PORO en su lugar?
La lógica detrás de esto es que sus ''acciones adicionales para ahorrar'' probablemente sean de lógica comercial. Esto me gusta mantenerme separado tanto de los modelos AR (que deberían ser lo más simples como sea posible) y de los controladores (que son molestos para probarlos 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 expandirlo, inyectando objetos de acción adicionales después de guardar
LoggedUpdater.save(user, [EmailLogger.new, MongoLogger.new])
Y para dar un ejemplo de los "extras". Sin embargo, es posible que desee spiffy un poco:
class EmailLogger
def call(msg)
#send email with msg
end
end
Si te gusta este enfoque, te recomiendo una lectura de la publicación del blog Bryan Helmkamps 7 Patterns .
EDITAR: También debo mencionar que la solución anterior permite agregar lógica de transacciones tambié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
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 maltratados.
Me gustaría advertir que no agregue demasiada lógica en sus devoluciones de llamada o simplemente mover el código para simular el comportamiento de un observador cuando ya existe una solución de sonido para este problema, el patrón Observador.
Si tiene sentido usar observadores, entonces, por supuesto, use observadores. Simplemente entienda que necesitará asegurarse de que su lógica de observador siga las prácticas de codificación de sonido, por ejemplo, SOLID.
La gema observadora está disponible en rubygems si desea agregarla a su proyecto https://github.com/rails/rails-observers
vea este breve hilo, aunque no incluye una discusión exhaustiva, creo que el argumento básico es válido. https://github.com/rails/rails-observers/issues/2
Eche un vistazo a las Concerns
Cree una carpeta en su directorio de modelos llamada inquietudes. Agrega un módulo allí:
module MyConcernModule
extend ActiveSupport::Concern
included do
after_save :do_something
end
def do_something
...
end
end
A continuación, incluya eso en los modelos en los que desea ejecutar after_save:
class MyModel < ActiveRecord::Base
include MyConcernModule
end
Dependiendo de lo que estés haciendo, esto podría acercarte sin observadores.
El uso de devoluciones de registros activos simplemente cambia la dependencia de su acoplamiento. Por ejemplo, si tiene modelA
y un observador de modelA
estilo de modelA
rails 3, puede eliminar CacheObserver
sin problemas. Ahora, en su lugar, di que A
debe invocar manualmente CacheObserver
después de guardar, que sería rails 4. Simplemente has movido tu dependencia para que puedas eliminar A
forma segura pero 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 un poco en por qué quieres / necesitas al observador, y 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 de controlador. De repente, debe inyectar 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 modifique las instancias a través de crear / actualizar acciones del controlador, más poder para usted, pero eso no es una suposición sobre una aplicación de rieles (considere formas anidadas, asociaciones de actualización de lógica de negocios modelo, etc.)
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
Están en un complemento ahora.
¿Puedo recomendar también github.com/krisleech/wisper 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
Mi alternativa a Rails 3 Observers es una implementación manual que utiliza una devolución de llamada definida dentro del modelo pero que logra (como agmin declara en su respuesta anterior) "voltear la dependencia ... acoplamiento".
Mis objetos heredan de una clase base que permite el registro de 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 la 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 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 de 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 se podrí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 observadores NotificationSender y ProfilePictureCreator a continuación definen los 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.
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 el título suena).
De vuelta en el día, todo era "modelo gordo, controlador flaco". Luego, los modelos de grasa se convirtieron en un dolor de cabeza gigante, especialmente durante las pruebas. Más recientemente, el impulso ha sido para los modelos delgados, la idea es que cada clase debe manejar una responsabilidad y el trabajo de un modelo consiste en conservar sus datos en una base de datos. Entonces, ¿dónde termina toda mi compleja lógica de negocios? En las clases de lógica de negocios: clases que representan transacciones.
Este enfoque puede convertirse en un atolladero (risueño) cuando la lógica comienza a complicarse. Sin embargo, el concepto es sensato: en lugar de desencadenar cosas de forma implícita con devoluciones u observadores que son difíciles de probar y depurar, desencadenan cosas de manera explícita en una clase que aplica lógica a la capa superior de su modelo.
Puede intentar https://github.com/TiagoCardoso1983/association_observers . Aún no se ha probado para los rieles 4 (que aún no se lanzó), y necesita un poco más de colaboración, pero puede verificar si es el truco para usted.
Vale la pena mencionar que el módulo Observable
de la biblioteca estándar de Ruby no se puede usar en objetos similares a registros activos ya que los métodos de las instancias changed?
y changed
chocará con los de ActiveModel::Dirty
.
github.com/krisleech/wisper es una gran solución. Mi preferencia personal para las devoluciones de llamadas es que los disparan los modelos, pero los eventos solo se escuchan cuando llega una solicitud, es decir, no quiero que se activen las devoluciones de llamadas mientras estoy configurando modelos en pruebas, etc. pero las quiero disparado cuando los controladores están involucrados. Esto es realmente fácil de configurar con Wisper porque puedes decirle que solo escuche los 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