Un truquito/ejemplo tonto de metaprogramación (en Ruby/Ruby on Rails)
Siempre que hablamos de metaprogramación parece que los ejemplos son un poco “de laboratorio”, más académicos que otra cosa, así que aquí tenemos un truquito rápido sacado de código real, que no es el invento de la pólvora ni nada que no pudiéramos hacer de una forma más convencional (y más larga y proclive a errores), pero la verdad es que la cosa queda bonita.
Pongamos que tenemos un modelo Article, que puede tener varios estados, como borrador, publicado, oculto, etc. Guardar ese estado como una cadena es una guarrería, así que un enfoque típico sería guardarlo como un entero, y tener una serie de constantes que hagan algo más legible el código:
class Article < ActiveRecord::Base PENDING = 0 PUBLISHED = 1 UNPUBLISHED = 2 ... end
Y luego hacer cosas como:
Article.find(:all, :conditions => [“status = ?”, Article::PENDING]) ... do_something if article.status == Article::PUBLISHED ... article.status = Article::UNPUBLISHED
Sin embargo, podemos hacerlo mucho mejor. Podéis llamarme pijo por preferir, con mucho, hacer cosas así:
Article.find_pending ... do_something if article.published? ... article.unpublished!
Programando uno puede ser todo lo pijo que quiera porque puede fabricarse sus propios caprichos, y escribir todos esos métodos:
def pending? self.status == PENDING end def pending! self.status = PENDING end def self.find_pending find(:all, :conditions => ["status = ?", PENDING]) end ...
Y así para cada estado, y otra vez más si en una semana se te ocurre añadir otro estado, y después comprobar que no has cometido ningún error en tooooodo ese código, ni te has saltado involuntariamente tus propias convenciones.
Lo malo es que yo además de pijo soy vago. Efectivamente, se puede hacer mejor, más DRY, y con menos posibilidad de errores. Pongo el código y después lo explico:
class Article < ActiveRecord::Base
STATUSES = {
:pending => 0,
:published => 1,
:unpublished => 2
}
STATUSES.each do |status, value|
define_method :"#{status}?" do
self.status == value
end
define_method :"#{status}!" do
self.status = value
end
end
class << self
STATUSES.each do |status, value|
define_method :"find_#{status}" do
find(:all, :conditions => ["status = ?", value])
end
end
end
end
Bien, en primer lugar hemos modificado la definición de los estados para hacerlo con un hash. ¿Para qué? Pues para poder iterar por él. En el primer caso, iteramos sobre la lista de estados y para cada uno definimos dos métodos, el acabado en ”?” y el acabado en ”!”, ambos de implementación trivial.
El truco está en define_method, que hace lo mismo que def sólo que es un método en sí mismo y por tanto nos da más flexibilidad, como por ejemplo poder usarlo de forma iterativa, o pasarle un parámetro (que será el nombre del método definido) de forma dinámica.
Después volvemos a repetir el truco pero a nivel de la clase, para definir el método de clase que queríamos tener. Y de esta forma, tendremos estos tres métodos para cada uno de los estados posibles. Y lo mejor es que, cuando la semana que viene alguien se de cuenta de que hace falta otro estado más, bastará añadirlo a la lista, y tendremos nuestros tres métodos, gratis.
Por cierto, esta es una implementación bastante sencilla del tema de los estados, aunque para el caso que me ocupaba más que suficiente. Si necesitáis algo un poco más complejo, os recomiendo que le echéis un ojo al plugin acts_as_state_machine, que hace esto que he explicado (aunque lo hace de otra manera), y muchas más features, como por ejemplo (para mí lo más chulo), callbacks que se ejecutan cuando el objeto pasa de un estado a otro.


