format internet:

…please wait (42% completed)…

usando method_missing y alias_method_chain

Posted by javier ramirez on September 16, 2007

En este post voy a comentar cómo usar algunas de las características que más me gustan de Ruby y Rails: la posibilidad de reabrir una clase en cualquier momento, poder capturar las llamadas a métodos que no existen en un objeto, y la posibilidad de hacer alias de métodos (y encadenarlos)

Para ilustrar todo esto y ver qué podemos hacer, vamos a modificar el comportamiento de la clase Hash de forma que pueda hacer lo siguiente


>> me={:name=>'javier'}
=> {:name=>"javier"}
>> me.name
=> "javier"
>> me.last_name
NoMethodError: undefined method `last_name' for {:name=>"javier"}:Hash
from (irb):18:in `method_missing'
from (irb):33
from :0

Si intento hacer eso sobre cualquier Hash, me devolvería un error directamente en la línea donde hago “me.name”, ya que la forma correcta de acceder a los elementos de una hash es mediante el operador []. La forma de acceder al campo “name” sobre una Hash cualquiera sería “me[:name]”. Con las modificaciones que vamos a realizar, conseguiremos un uso un poquito más conciso (a la manera que tiene ActiveRecord con los atributos de nuestro modelo).

Añadir el método ‘name’ a mi Hash sería muy simple. Aprovechándonos de que Ruby me permite reabrir una clase en cualquier momento, podría hacer simplemente esto

class Hash
  def name
    self[:name]
  end
end

Esto sería perfecto, pero implica que conocemos previamente el nombre de los campos a los que queremos acceder y además necesitamos escribir un método por campo. No parece una buena opción. Si conocemos previamente el nombre de los campos podríamos usar Struct y olvidarnos.

Entonces, si no sabemos previamente el nombre de los campos, ¿cómo sabemos qué métodos implementar? La respuesta es simple: no lo sabemos y no lo podemos saber. Pero… y ¿qué hacemos entonces?

Aquí es donde entra en juego una de las características que más me gustan de Ruby. Cuando un objeto recibe un mensaje que no entiende, Ruby automáticamente invoca a un método especial del Kernel. Este método es method_missing. Nota: ‘mensaje’ es el nombre que se usa en teoría de objetos cuando hablamos de un método. Realmente lo que tenemos son objetos que se envían mensajes entre ellos.

El método method_missing es un método del Kernel, pero si yo reabro una clase y defino el método, cuando se invoque a method_missing para esa clase conseguiré interceptar todas las llamadas a métodos no existentes. El intérprete de ruby nos va a pasar dos parámetros, el primero es el nombre del método, y el segundo son los parámetros que se usaban en la invocación, si los había.

Ahora ya podríamos hacer lo siguiente

class Hash
  def method_missing(method_name,*args)
    self[method_name]  if self.member?(method_name) 
  end
end

Y esto ya casi hace lo que queremos, pero hay todavía un pequeño detalle. Si pedimos un campo que no existe, vamos a obtener un nil en lugar de una excepción. Esto no tiene porqué ser malo, y de hecho podría ser el comportamiento deseado. Pero no es lo que planteábamos originalmente. Queremos que si el campo existe nos lo devuelva, y si no existe se comporte como antes de sobreescribir el método.

Para poder hacer este tipo de cosas en Ruby, usamos la posibilidad de hacer alias de los métodos. La forma habitual es hacer un alias del método original con un nombre tal que loquesea_old y después desde el método nuevo que escribimos podemos invocar al antiguo mediante el alias definido. Esto tiene un problema inherente a la propia arquitectura de una aplicación en Ruby. Como las clases por definición se pueden reabrir en cualquier momento, cualquiera puede redefinir un método en cualquier momento, con lo que corremos el riesgo de estar ocultando un método sobreescrito previamente al hacer un alias.

Para evitar este tipo de problemas, en el código interno de rails se usa un patrón que funciona bastante bien. Cuando se define un nuevo método que va a ocultar a uno viejo al que se hará alias, se usa un sufijo para indicar qué tipo de funcionalidad implementa. Después se definen los alias de esta forma

alias :method_missing_without_access_by_key, :method_missing
alias :method_missing, :method_missing_with_access_by_key

A la hora de definir nuestro método nuevo, lo definiremos con el patrón nombre_antiguo_with_sufijo_indicando_funcionalidad. En nuestro caso se definiría como “method_missing_with_access_by_key”. De esta forma, aunque varios plug-ins diferentes intenten hacer alias de un mismo método, mientras no colisione el sufijo con uno que ya existe no tendremos problemas, con lo que se reduce el riesgo de tener conflictos de nombres.

Como este patrón es muy interesante, desde la versión 1.2 está implementado como parte del core de rails, bajo el nombre “alias_method_chain”. Podemos hacer simplemente

alias_method_chain :method_missing, :access_by_key

Esto busca un método que se llame method_missing_with_access_by_key y aplica los dos alias que hemos visto antes.

Así que la versión final de nuestro pequeño hack quedaría del siguiente modo

class Hash
  def method_missing_with_access_by_key(method_name,*args)
  self.member?(method_name) ? self[method_name] : method_missing_without_access_by_key(method_name,*args)
end

alias_method_chain :method_missing, :access_by_key
end

Todavía hay espacio para hacer alguna pequeña mejora, que probablemente explicaré en un post más adelante.

searchwords: method_missing, alias_method_chain, ruby alias method, Struct

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: