Method_missing(): ¿me ayuda a eliminar código duplicado? – espera, déjame que te lo explique


Metaprogramming RubyEn esta ocasión me gustaría hablar del method_missing(), me lo he encontrado en varias ocasiones y siempre lo he mirado de lado, no sea que pase algo.

Fuera de bromas, es otro de los temas, que tenía ganas de comentar y que mejor ocasión después de ver un vídeo hablando de este tema, leer sobre ello y preguntar a Paolo Perrotta sobre su charla en vídeo, que recomiendo ver por su alto contenido de información, titulado “The Revenge of method_missing()”, espera no te preocupes en buscarlo, que lo pongo al final del post, vamos por partes,  voy a tratar de explicar a mi manera con la ayuda de la información proporcionada por Paolo, tal como yo entendido y cuando debemos o no, utilizar este método. Pero lo principal es conocer, para poder saber aplicarlo.

Comencé a leer hace tiempo ya (vuelvo a ver capítulos de vez en cuando como en este caso), el libro de Paolo Perrotta @nusco en twitter, “Metaprogramming Ruby” que comenta en el capítulo 2 sobre los métodos, por cierto muy interesante, como todo el libro y comienza la problemática, cuando tenemos definidos métodos, cuyo código está repetido en otros métodos dentro de la misma clase. En ese caso podemos pensar ¿method_missing() puede ayudarnos a que nuestro código no se repita? busquemos la respuesta y el posible razonamiento.

Para entender de lo que estamos hablando, he creado una clase de ejemplo básica,  una variante de lo que Paolo comenta en el vídeo, pero en la esencia es lo mismo para entenderlo:

class Band

def initialize(band_id,source)
@id = band_id
@source = source
end

def drump
         name = @source.get_name_drummer(@id)
         result = “Name of Drummer: #{name}”
end

  def guitar
    name = @source.get_name_guitarist(@id)
    result = "Name of Guitarist: #{name}"
end
end

Como puedes ver, tenemos código duplicado en los métodos, para extraer la información del nombre de cada componente de nuestro grupo. Pero para seguir, echemos un vistazo a cómo funciona el recorrido de los objetos en Ruby. Primero, nos encontramos nuestra propia clase Band, para ir subiendo a los ancestros por Object -> Kernel -> Basic object. Entonces cuando llamamos a un método de clase, primero va a ir a nuestra clase Band a ver si lo encuentra allí, si no lo encuentra, comienza a viajar hacia arriba, Object, si no lo encuentra va a Kernel y si no lo encuentra, llegara hasta Basic Object y allí se encuentra con method_missing(). Este fue mi primer encuentro con este método, que supongo que como todos. Si ahora reescribimos el method_missing() sobre nuestra clase Band, y reemplazamos todos nuestros métodos nos queda algo así:

class Band
   def method_missing(method, *args)
        puts "Method was called: #{method}(#{args.join(', ')})"
        puts "(an a block also)" if block_given?
   end
end 

band = Band.new
band.talking_about_my_generation('My', 'Generation') do
      # a block
end

Bueno, ya tenemos lo que estábamos buscando, nuestros métodos ya no están duplicados. Esto es lo más extraño, si te paras a pensarlo por un momento, cuando creas la instancia de la clase y después llamas al método talking_about_my_generation con dos parámetros y después vas a la clase y no lo ves definido por ninguna parte, choca así a simple vista, verdad?, bueno pues esto es a lo que se denomina métodos fantasmas.

Hagamos un alto en el camino, ¿podríamos resistirnos al method_missing()?. Pensemos en las implementaciones de métodos proxy, que en definitiva son aquellas que la clase hija sabe de los métodos de su clase padre.Veamos:

class A 
    def hi 
         puts "Hi from #{self.class}" 
    end 
end

class B 
       def initialize 
          @b = A.new 
       end

       def method_missing(method_name, *args, &block) 
            @b.send(method_name, *args, &block) 
       end 
end

A.new.hi #=> Hi from A

B.new.hi #=> Hi from A

Ahora puedes comprobar en este ejemplo, que navegamos en dos únicas clases hasta que cuando llamamos al método hi de la clase instanciada B.new, el recorrido hasta saber llegar al método hi es corto, pero si lo trasladamos al entorno de Rails y concretamente a ActiveRecord posiblemente tardemos algo más en llegar. En este caso tenemos los métodos de Active record de find_by_name, que en la versión 4 de Rails han cambiado esta mágina

Ahora te voy a presentar a un buen aliado, define_method.¿Qué hace este método dinámico? y ¿cómo nos ayuda?, pues nos permite es definir dinámicamente a un método o más y lo mejor de todo es que cuando se cargan las clases, los métodos ya los tenemos disponibles, con lo que ya te puedes imaginar, no necesita subir a sus ancestros de la clase cuando se llama a los métodos creados. Por lo que podríamos decir que define_method podría ser un buen compañero de viaje.

class A
  def hi
    puts "Hi."
  end
end

class B < A
    define_method(:hello,instance_method(:hi))
end

B.new.hi #=> "Hi."
B.new.hello #=> "Hi."

Llegado a este punto, me planteo la misma pregunta que se hace Paolo Perrotta, ¿qué debo utilizar entonces, métodos dinámicos o métodos fantasmas?. Tenemos que seguir investigando algunas cosas más.

class B
  def initialize
    #..............
  end

  def it_is_my_time?
    # ..............
  end

  def method_missing(name, *args)
     return "Hi for all world" if it_ismy_time?
  end
end

B.new.what_is_hi #

¿qué ocurre aquí? pues que miramos, pero no vemos y eso es un problema. Fíjate , que he cometido un error al escribir la llamada al método it_is_my_time? y lo he puesto it_ismy_time? y el problema que acarrea hacerlo de esta forma (no me refiero al error) es que cuando hagamos B.new.what_is_hi, nuestra clase se queda inmersa en un bucle infinito hasta que nos de un error de stack. Para solucionarlo, preguntamos si responde antes de pasar a la siguiente línea de código:

super unless @my_time.respond_to? name si responde, seguimos con el código o en caso contrario que no exista, evitamos el stack. Pero….hagamos una cosa, vamos a probarlo con los módulos:

module SayGoodMorning    
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^goodmorning/) => cuidado!!
    end
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^goodmorning/)
            puts "Good Morning, #{method}"
        else
            super.method_missing(method, *args, &block) => cuidado!!
        end
    end
end

module SayGoodNight
    def respond_to?(method)
        super.respond_to?(method) || !!(method.to_s =~ /^goodnight/) => cuidado!!
    end 
    def method_missing(method, *args, &block)
        if (method.to_s =~ /^goodnight/)
            puts "Goodnight, #{method}"
        else
            super.method_missing(method, *args, &block) => cuidado
        end 
   end 
end 

class ObjectA 
  include SayGoodMorning 
end 

class ObjectB 
  include SayGoodNight 
end

Cuando instanciamos las clases:

1.9.3p327 :038 > ObjectA.new.goodmorning_there
GoodMorning, goodmorning_there
=> nil
1.9.3p327 :039 > ObjectB.new.goodnight_there
GoodNight, goodnight_there
=> nil

Hasta aquí……. bien, pero hay un error de concepto en el planteamiento, pero hasta ahora no ha pasado nada fuera de lo normal ¿qué pasaría si hacemos una nueva clase e instanciamos y volvemos a preguntar?:

class ObjectC
    include SayGoofMorning
    include SayGoodNight
end

Ahora si lo vemos en consola tenemos:

1.9.3p327 :069 > ObjectC.new.goodnight_other
GoodNight, goodnight_other
=> nil
1.9.3p327 :070 > ObjectC.new.goodmorning_other
GoodMorning, goodmorning_other
NoMethodError: private method `method_missing’ called for nil:NilClass
from (irb):24:in `method_missing’
from (irb):70
from /usr/local/rvm/rubies/ruby-1.9.3-p327/bin/irb:13:in `<main>’

Pero que curiosa la respuesta obtenida en la clase C. Bueno lo que está pasando es que lo estamos planteando mal y además es un error de concepto de funcionamiento de super. Cuando hagamos una llamada a super, lo que estamos haciendo es que debe estar dentro de un método y dentro de ese método, si hacemos la llamada a super tal cual o le pasamos parámetros, no necesitamos poner el nombre del método, es decir, no lo llamaremos super.method_missing,  lo que hace super es subir al padre y ver si hay un método dentro del padre que se llame igual que desde el método del hijo llamante.

Es lógico que tengamos estos resultados, date cuenta que tenemos ambos módulos con los mismos métodos por lo que estamos haciendo es una sobre escritura del módulo SayGoodMorning y predomina el segundo módulo SayGoodNight.

Buscamos más pegas. Ahora imagina que tenemos una clase cualquiera que dentro dispone de un método que lo llamamos display(), cuando hagamos en la segunda clase la instancia dicha clase, obtenemos algo que no esperamos, la creación de un objeto y no la visualización del método display, que es lo que supuestamente esperamos. El motivo es que display es un método que existe en Kernel, esto es otro de los problemas que nos podemos enfrentar, la creación de un método fantasma falso. Para que esto no nos pase podemos encontrar un nuevo camino, con BasicObject elimina todos los métodos y nos quedamos con los de la clase y ¿asunto resulto? bueno hasta cierto punto:

Class InfoDesk < BasicObject

def initialize
@time = TimeTable.new
end

def method_missing(name, *args)
super unless @time.respond_to? name

return “it’s time to lunch” if Clock.lunch_time?
@time.send(name, *args)
end
def respond_to?(method)
@time.respond_to?(method) || super
end
end

InfoDesk.new.display # => TimeTable#display() called

Como hemos decidido que la clase se resetee o se quede en blanco como un inicio en blanco, ahora debemos implementar algo que nos permita definir nuestra clase y esto lo conseguimos con un singleton_class:

 

class InfoDesk < BasicObject

        def initialize
             @time = TimeTable.new
        end

       def method_missing(name, *args)
             super unless @time.respond_to? name
            singleton_class.send :define_method, name do
                 return “it’s time to lunch” if ::Clock.lunch_time?
                 @time.send name
            end
     def respond_to?(method)
          @time.respond_to?(method) || super
     end
end

InfoDesk.new.flights # => Systemstackerror

Pero esta solución, tampoco nos sirve de mucho, ya que al eliminar las clases con BasicObject no disponemos de singleton_class, por lo que no es bueno seguir este camino. Podríamos encontrarnos en algún caso un method_missing() para implementar un decorator:

module Decorator
def initialize(component)
@component = component
end

    def method_missing(meth, *args)
          if @component.respond_to?(meth)
             @component.send(meth, *args)
         else
             super
         end
    end

    def respond_to?(meth)
          @component.respond_to?(meth)
      end
end

class Coffee
        def cost
             2
         end

        def origin
             “Colombia”
        end
end

class Milk
        include Decorator

        def cost
            @component.cost + 0.4
        end
end

coffee = Coffee.new
Sugar.new(Milk.new(coffee)).cost # 2.6
Sugar.new(Sugar.new(coffee)).cost # 2.4
Sugar.new(Milk.new(coffee)).origin # Colombia
Sugar.new(Milk.new(coffee)).class # Sugar

Así que la conclusión final es:

  • method_missing() no nos proporciona un camino correcto para eliminar código duplicado
  • Un buen aliado puede ser define_method

Finalmente te dejo el vídeo de Paolo que merece mucho la pena que lo veas tranquilamente y deja comentario para poder enriquecer este post:

The Revenge of method_missing()

About these ads

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s