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”

Gravatar de Carmen María Pelayo

Carmen María Pelayo
27 de Mayo de 2010 a las 7:58 pm    

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.

Gravatar de Jose Luis

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

Más entradas en Negonation Blog