Gestión de eventos en Rails: Observers
Todos los desarrolladores Rails utilizamos los callbacks de ActiveRecord. El uso de estos callbacks da mucho juego a nuestros modelos: validaciones, tratamiento de datos, operaciones sobre modelos relacionados, envío de e-mails automáticos…
Al principio, cuando uno empieza a desarrollar en Rails, todas esas operaciones que comentaba solemos realizarlas en el controllador. Sin embargo, a medida que vamos aligerando nuestros controladores y engordando nuestros modelos la cantidad de callbacks que empleamos en nuestros modelos va aumentando. Esto es algo bueno, pero en ocasiones acabamos con un montón de callbacks en el modelo que no tienen que ver con el modelo en si.
Para extraer de los modelos todo ese código disparado por callbacks que no tiene que ver directamente con el modelo Rails proporciona los observers.
Los observers por dentro
En este post vamos a ver cómo funcionan los observers de rails por dentro. Para ello es necesario saber cómo se emplean así que, si no los has usado nunca, deberías echarle un vistazo a la documentación
El módulo Observable en Ruby
Ruby implementa su librería estándar algunos módulos sobre patrones de diseño habituales. Uno de estos módulos es Observable.
El módulo Observable implementa el patrón Observer también llamado Publicador-Sucriptor. En este patrón un objeto (el publicador o la fuente) informa a un conjunto de objetos interesados (los suscriptores) cuando su estado cambia. Para ello nos proporciona una serie de métodos para registrar suscriptores y para notificarles los cambios de estado.
Veamos un ejemplo de uso del módulo Observable:
require 'observer' class MonitorSistema include Observable def run ultimo_espacio_libre = nil loop do espacio_libre = Disco.espacio_libre puts "Espacio libre en disco: #{espacio_libre}MB" if espacio_libre != ultimo_espacio_libre changed notify_observers(espacio_libre) ultimo_espacio_libre = espacio_libre end sleep(60) end end end class MantenimientoDisco def initialize(limite) @limite = limite end def update(espacio_libre) if espacio_libre < @limite puts "-- Limpiando archivos temporales. #{espacio_libre}MB de espacio libre" end end end class AlertaPocoEspacio def initialize(limite, email) @limite = limite @email = email end def update(espacio_libre) if espacio_libre < @limite puts "-- Notificando a #{@email}. #{espacio_libre}MB de espacio libre" end end end monitor = MonitorSistema.new monitor.add_observer(MantenimientoDisco.new(700)) monitor.add_observer(AlertaPocoEspacio.new(700, 'pedro@ejemplo.com')) monitor.add_observer(AlertaPocoEspacio.new(600, 'antonio@ejemplo.com')) monitor.run
El ejemplo me parece que es bastante ilustrativo, pero mejor aclararlo algunos detalles
El módulo Observable define una serie de métodos que podéis consultar en la documentación del módulo. En nuestro ejemplo usamos tres de ellos:
- changed(state=true): cambia el estado del objeto.
- notify_observers(*args): si el estado es true, invoca al método update de cada suscriptor con los mismos argumentos.
- add_observer(observer): registra un nuevo suscriptor.
Veamos una ejecución de ejemplo del script anterior:
Espacio libre en disco: 1011MB Espacio libre en disco: 821MB Espacio libre en disco: 880MB Espacio libre en disco: 625MB -- Limpiando archivos temporales. 625MB de espacio libre -- Notificando a pedro@ejemplo.com. 625MB de espacio libre Espacio libre en disco: 730MB Espacio libre en disco: 570MB -- Limpiando archivos temporales. 570MB de espacio libre -- Notificando a pedro@ejemplo.com. 570MB de espacio libre -- Notificando a antonio@ejemplo.com. 570MB de espacio libre Espacio libre en disco: 716MB Espacio libre en disco: 841MB Espacio libre en disco: 1016MB
ActiveRecord: Publicador y suscriptor
ActiveRecord tiene dos clases involucradas en la gestión de eventos:
- ActiveRecord::Base: incluye el módulo Observable de Ruby y notifica a los suscriptores todos los eventos a los que se pueden asociar callbacks.
- ActiveRecord::Observer: es la clase base para todos los observers y define el método update.
Cuando un modelo (como sabemos, subclase de ActiveRecord::Base ) cambia de estado invoca a, además de los callbacks dentro de la propia clase, el método notify_observers con los siguientes argumentos:
- El nombre del evento: before_validation, after_save, after_destroy…
- El objeto ActiveRecord.
Tal y como hemos visto, al invocar notify_observers en el modelo se invoca update en todos los observers suscritos a dicho modelo. En los observers de rails el método update invocará a su vez, en caso de que exista, al método de la clase que tenga el nombre del evento notificado y le pasará como argumento el objeto que ha cambiado de estado.
# Send observed_method(object) if the method exists. def update(observed_method, object) #:nodoc: send(observed_method, object) if respond_to?(observed_method) end
Inicialización de observers en Rails
Ya hemos visto cómo notifica ActiveRecord::Base los eventos a los observers. No obstante, no hemos visto cómo se suscriben los observers (ya sabéis, la llamada al método add_observer del módulo Observable).
En el caso de los observers de rails la llamada a add_observer se realiza en el constructor de ActiveRecord::Observer:
# Start observing the declared classes and their subclasses. def initialize Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass } end
Sin embargo, tal y como dice la documentación, para utilizar los observers no instanciamos objetos, sino que los configuramos en el archivo environment.rb (config.activerecord.observers_). Esto es así para delegar en Rails la responsabilidad sobre cuándo inicializar los observers.
No vamos a entrar en cuándo los inicializa, sin embargo sí que diremos cómo lo hace.
La clase ActiveRecord::Observer sigue un patrón Singleton. Este patrón garantiza que como máximo habrá una instancia de una clase. Para ello se cambia la visibilidad del constructor para que sea privado y se define un método que cree devuelva el único objeto que instancia la clase.
Este patrón también viene en la librería estándar de Ruby con el módulo Singleton. Este módulo cambia la visibilidad del constructor y define el método instance.
Por lo tanto, Rails emplea el método instance para suscribir los observers a los modelos correspondientes.
Sabiendo esto…
Sabiendo estos detalles sobre la implementación del patrón Publicador-Suscriptor en ActiveRecord y su inicialización de Rails tenemos los conocimientos técnicos necesarios para desarrollar nuestros propios observers.
Nosotros tuvimos la necesidad de desarrollar nuestros propios observers al querer tener una única clase que se suscribiese varios modelos, pero atendiendo a eventos distintos según el modelo.
En un post próximo, aprovecharemos lo que hemos explicado aquí para ver los detalles de nuestros observers.
Happy Hacking!
Por Ernesto Jiménez
Guardado en: Programación, Rails, Ruby | 2 comentarios » | 3 de Marzo de 2008
2 Comentarios en “Gestión de eventos en Rails: Observers”
Buenas tardes,
Felicitaciones por su artículo, me va hacer de mucha ayuda. Sin embargo, quería si puedo utilizar un observers, tengo un modulo de préstamos y requiero enviar un email cuando se vence el préstamo, cómo podría realizar esta opción? gracias.

Jose Luis
31 de Mayo de 2010 a las 6:22 pm
Hola Carmen,
Entiendo que el desencadenante sería el momento de tiempo en el que el préstamo vence, no alguna acción asociada a tu aplicación.
Algo sencillo sería montar un rake task y ejecutarlo mediante cron una o varias veces al día. Buscar los préstamos vencidos y enviarles un email.
Un tutorial muy bueno sobre rake:
http://www.rodolinux.com.ar/node/60
Carmen María Pelayo
27 de Mayo de 2010 a las 7:58 pm