Saturday, January 19, 2013

DRY: Metaprogramación


Ruby es un lenguaje dinámico y precisamente la metaprogramación digamos que no es más que escribir programas que escriben programas. De ahí el nace el concepto de DRY ó "no te repitas a ti mismo". Veamos esto con un ejemplo sencillo:

Imaginemos que tenemos un Tweet que puede tener 3 estados: pendiente, publicado y censurado.
En un primer momento se nos puede ocurrir definir 3 constantes para la clase Tweet que identifiquen cada uno de los estados anteriores:

class Tweet < ActiveRecord::Base

   PENDING = 0

   PUBLISHED = 1

   CENSURED = 2

end 


Ahora se nos podría ocurrir ir definiendo todos aquellos métodos de clase que necesitaríamos para extraer de base de datos todos los tweets en estado de pendientes, publicados o censurados. O incluso métodos de instancia para saber el estado de un tweet en concreto. Algo de esta manera:

class Tweet < ActiveRecord::Base

   PENDING = 0

   PUBLISHED = 1

   CENSURED = 2

   def self.all_pending
     find(:all, :conditions => { :status => PENDING }
   end

  def self.all_published
    find(:all, :conditions => { :status => PUBLISHED }
  end

  def self.all_censured
    find(:all, :conditions => { :status => CENSURED }
  end

  def pending?
    self.status == PENDING  
  end

  def published?
    self.status == PUBLISHED  
  end

  def censured?
    self.status == CENSURED  
  end

end

Sin embargo se puede hacer mejor, más DRY, y con menos posibilidad de errores usando métodos dinámicos. Veamos el código para entenderlo de manera rápida y sencilla:

class Tweet < ActiveRecord::Base 
    STATUSES = { 
        :pending => 0, 
        :published => 1, 
        :censured => 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_name|
         define_method "all_#{status_name}" do
           find(:all, :conditions => { :status => status_name }
         end
      end
    end
end

Como se puede ver no repetimos tanto código que es lo que nos hace ser más DRY. Definimos el conjunto de estados como una Hash para poder recorrer ese conjunto y usamos define_method que nos da más flexibilidad que def ya que nos permite pasarle un parámetro, en este caso el estado, e ir definiendo de manera dinámica los distintos métodos que comentábamos anteriormente.

No comments:

Post a Comment