Ruby,Rails: “metodos encadenados”

Una de las cosas que mas me gustan de Ruby es su dinamismo y la posibilidad de “abrir” las clases y modulos para añadir o redefinir metodos totalmente dinamicamente y en tiempo de ejecucion.

Esta funcionalidad junto con la posibilidad de saber cuando un modulo ha sido incluido en una clase nos permite añadir funcionalidad a las clases que incluyen el modulo sin que estan tengan que percibirlo.

Un ejemplo:

class Module
  def included(base)
    p "Included module " + name
 
    without = "test_method_#{name}_without_filters".to_sym
    with ="test_method_#{name}_with_filters".to_sym
    base.class_eval do
      alias_method without, :test_method
      alias_method :test_method, with
    end
 
  end
end

Lo que he hecho es redefinir el metodo included para que todos los modulos indiquen que ha sido incluidos y que hagan un alias sobre el metodo “test_method“.

Hay que tener en cuenta que alias_method no sobreescribe el metodo original, sino que hace una guarda una referencia a este, de forma que aun pueda ser invocado.

module A
 
  def test_method_A_with_filters
    p "Doing something really important in module A"
    test_method_A_without_filters
  end
 
end
 
module B
 
  def test_method_B_with_filters
    p "Doing something really important in module B"
    test_method_B_without_filters
  end
 
end
 
 
class Test
 
  def test_method
      p "Inside method of class A"
 
      p "Backtrace:"
      caller.each do |text| p text end
      return
  end
 
  include A
  include B
end

Creamos los modulos, la clase e incluimos los primeros en dicha clase.Hasta aqui nada nuevo bajo el sol.

Probemos entonces el codigo en irb,

madtrick:~/programacion/ruby madtrick$ irb
irb(main):001:0> require "test30"
"Included module A"
"Included module B"
=> true
 
irb(main):002:0> Test.new.test_method
"Doing something really important in module B"
"Doing something really important in module A"
"Inside method of class A"
 
"Backtrace:"
"./test30.rb:14:in `test_method_B_without_filters'"
"./test30.rb:32:in `test_method'"
"(irb):2:in `irb_binding'"
"/opt/local/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'"
":0"
=> nil

Si hechamos un vistazo a la salida generada en irb,

irb(main):001:0> require "test30"
"Included module A"
"Included module B"
=> true

Los modulos se incluyen en la clase en el orden deseado

irb(main):002:0> Test.new.test_method
"Doing something really important in module B"
"Doing something really important in module A"
"Inside method of class A"

Ahora creamos un objeto de tipo Test e invocamos sobre él el metodo “test_method”…!Pero que ha pasado aqui¡!De donde salen esa lineas¡No no pongamos nerviosos y averigüemos que pasa.

Cuando redefinimos el metodo included de la clase module, realizamos unos alias.
Para el modulo A:

  • Nuevo nombre : test_method_A_without_filters,nombre antiguo: test_method
  • Nuevo nombre : test_method, nombre_antiguo test_method_A_with_filters

Para el modulo B , lo mismo pero cambiando la A por una B.

El resultado de estos alias es el que vemos en la salida de irb, al invocar sobre un objeto de la clase Test
su metodo de instancia “test_method” lo primero que vemos no es “Inside method of class A” sino “Doing something really important in module B“,seguido de un “Doing something really important in module A” para finalmente ver nuestro esperado “Inside method of class A“,esto es asi porque:

  1. El ultimo modulo incluido fue el B, por tanto su alias “sobreescribioo” al creado por el modulo A.Es decir al hacer Test.new.test_method en realidad estamos invocando a test_method_B_with_filters
  2. Tras ejecutar este metodo, invocamos al metodo “original”, que en este caso no es el definido en la clase, sino el alias realizado por el modulo A
  3. Tras ejecutar este ultimo alias, volvemos a invocar al metodo original (alias de “test_method_A_without_filters”) quien finalmente si que es el metodo de la clase Test

Utilizando caller podemos saber la stacktrace de nuestro programa y asi saber cual fue la direccion de los mensajes.Esto lo podemos ver en este trozo de la salida de irb,

"Backtrace:"
"./test30.rb:19:in `test_method_B_without_filters'"
"./test30.rb:30:in `test_method'"
"(irb):2:in `irb_binding'"
"/opt/local/lib/ruby/1.8/irb/workspace.rb:52:in `irb_binding'"
":0"

Teniendo en cuenta que las primeras lineas indican los ultimos mensajes.Tenemos que:

En la linea 30 vamos enviar un mensaje tras recibir uno con destino a “test_method” alias de “test_method_B_with_filters“,

28
29
30
31
  def test_method_B_with_filters
    p "Doing something really important in module B"
    test_method_B_without_filters
  end

En la linea 19 vamos a enviar un mensaje tras recibir uno con destino “test_method_B_without_filters
el cual es un alias de “test_method“,

17
18
19
20
 def test_method_A_with_filters
    p "Doing something really important in module A"
    test_method_A_without_filters
  end

Como se puede apreciar es una forma bastante sencilla de añadir funcionalidad sin influir para nada en las clases.Y de aqui es de donde saque el titulo, porque literalmente estamos encadenando metodos.

Para terminar
Si alguien le extraña la coletilla _filters que utilize para hacer los alias , tiene su explicacion.Ultimamente estoy curioseando en el codigo de Rails, y para poder seguir un orden me voy guiando por el orden el que se procesan las peticiones ,gracias al debugger para webrick y otros servidores.Bueno la cosa es que llegue al fichero filters.rb donde me encontre con este codigo,

 module InstanceMethods # :nodoc:
      def self.included(base)
        base.class_eval do
          alias_method_chain :perform_action, :filters
          alias_method_chain :process, :filters
        end
      end
...

¿alias_method_chain?, no me sonaba para nada, asi que tras una consulta a google llegue a esto:

All over the internals of Rails you’ll find code like this in a module:

module Layout #:nodoc:
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
alias_method :render_with_no_layout, :render
alias_method :render, :render_with_a_layout
# … etc
This makes it so that when the module is included into the base class, it adds behavior onto some method in that class without the method having to be aware of the fact that it’s being enhanced

En resumidas cuentas, que alias_method_chain hace lo que acabos de hacer nosotros pero un poquillo mejor ya que se puede aplicar a cualquier metodo, no solo a test_method como en nuestro caso.

Volviendo a lo de la coletilla _filters, la explicacion es que donde vi el primer ejemplo de alias_method_chain fue en el codigo indicado arriba y entonces fue lo primero que se me ocurrio.

Category: programacion, rails, ruby | Tags: , , , One comment »

One Response to “Ruby,Rails: “metodos encadenados””

  1. Andion

    Genial el ejemplo!


Leave a Reply