Confident Ruby: 5 tips que aprendí de Avdi Grimm

Mantenerse al día es una de las responsabilidades de los desarrolladores, muchas veces no se trata del conocimiento más reciente, si no de los pequeños consejos que aprendes de personas mas experimentadas. En esta ocasión de la mano de Avdi Grimm con su magnífico libro Confident Ruby descubrí nuevas formas de estructurar mi código de manera que transmita mi intención al programarlo, así que déjame platicarte los consejos que causaron más impacto en la forma que programo.

Antes de continuar, me gustaría hacer un pequeño resumen de lo que trata Confident Ruby, el concepto del libro trata de como los métodos se dividen en cuatro partes, comenzando por recolectar la información, realizar el trabajo, regresar el resultado del trabajo y por último el manejo de errores, en cada una de estas etapas Avdi nos enseña con ejemplos formas de refactorizar nuestro código de forma que sea más organizado y limpio. Definitivamente una lectura obligada para aquellos que quieran mejorar su programación en ruby sin caer en cosas más avanzadas como patrones de diseño y arquitecturas.

Ahora si, comencemos con la lista de consejos que cambiaron mi forma de programar:

1. Guard clauses

Probablemente ya lo has escuchado o visto en el código sin saberlo, incluso rubocop(la herramienta para analizar el código en nuestra aplicación) nos hace recomendaciones sobre esto. Guard clauses es una forma para evitar que se ejecute código innecesario, esto es, que no se cumplan las condiciones que nuestro método necesita para su correcta ejecucion.

Supongamos que tenemos un método que requiere información personal de un usuario, no podemos asumir que el usuario llenó toda la información correctamente, aun cuando los campos sean requeridos. Nosotros podemos asegurarnos de que los valores que ocupamos se encuentran presentes antes de continuar con la parte del método que ejecuta el trabajo de forma que detengamos su ejecución si sabemos que no se ejecutara correctamente. Por ejemplo:

def documents(list)
  return unless list
  user.documents = list.map(&:url)
end

No solo nos podemos limitar a validar la información que necesitamos, también podemos regresar un mensaje de forma que sea claro porque no fue posible ejecutar el método, por ejemplo:

def documents(list)	
  return “Invalid list” unless list
  # … method code
end

Incluso validar que el tipo de dato sea el correcto:

def documents(list)	
  raise TypeError, "Invalid list" unless list.is_a?(Date)
 # … method code
end

2. Fetch

Cuando trabajamos con hashes siempre terminamos de una forma u otra validando la presencia de un valor, haciendo cosas creativas como my_hash['key'] && my_hash['key']['another_key'] por mencionar una, sin embargo existen mejores formas de hacerlo más legibles.

Imaginemos que tenemos un hash de atributos el cual no tenemos garantía de que contiene toda la información que necesitamos, normalmente haríamos algo así:

def update_user(attributes)
  email = attributes[:email]  
  raise ArgumentError, 'Email must be supplied' unless email
  ...
end

Sin embargo, la clase de Hash tiene un método que nos ayuda hacer exactamente lo mismo de forma más elegante, me refiero a fetch que verifica si la key está definida en el hash, por ejemplo:

def update_user(attributes)
  email = attributes.fetch(:email) 
  ...
end

Opcionalmente, en caso de que no exista el valor, podemos ejecutar un bloque de código que pueda levantar una excepción o regresar un valor predeterminado.

def update_user(attributes)
  email = attributes.fetch(:email) do
    raise ArgumentError, 'Email must be supplied'
  end
  ...
end


def update_user(attributes)
  email = attributes.fetch(:email, “Invalid email”)
  ...
end

3. Representar casos especiales como objetos

Siempre hay escenarios donde nuestro código debe de considerar casos diferentes, por ejemplo si tuviéramos un sistema donde los usuarios tienen diferentes niveles de acceso o membresía tales como Basic, Pro, etc. Nuestra aplicación debería de adaptarse a los comportamientos que cada uno tiene disponible sin tener que validar esto en cada lugar donde se necesita, por ejemplo:

if current_user.level.pro?
  render_view_more_button
else
  render_upgrade_membership_button
end

Si bien al inicio no habrá mucha diferencia, a medida que nuestra aplicación crezca nos toparemos cada vez más con estos fragmentos de código en las vistas, modelos y controladores haciendo increíblemente difícil el cambiar la condición o incluso agregar otra opción más como un nivel nuevo de membresía. Para solucionar este tipo de problemas podemos hacer uso de una clase para cada tipo de membresía de forma que la lógica propia de cada nivel está encapsulada en el mismo objeto, por ejemplo:

class Pro
  def view_more 
    render_view_more_button	
  end 	

  def visible_courses 
    Course.all
  end
  ...
end

class Basic
  def view_more 
    render_upgrade_membership_button
  end 	

  def visible_courses 
    Course.find_by(status: 'free') 
  end	
  ...
end

Podemos definir un método en nuestra clase de usuario que se encargue de definir el nivel de la membresía e evitando que el código necesite saber estos detalles de esta forma:

class User
  def membership
    if current_user.level.pro?
      Pro.new()
    else
      Basic.new() 
    end
  end
end

Usándolo de esta manera podemos descartar el uso repetitivo del if:

current_user.membership.view_more

4. Uso de yield

Una de las partes que se me hicieron más interesantes fue el uso de yield en formas diferentes a las que ya conocía, en este caso el manejo de errores de forma agnóstica como se se muestra a continuación:

def fill_information(fields)
  fields.each do |field|
    begin 
      insert_data(field)
    rescue => error 
      return yield(field, error) if block_given?
    raise
    end
  end
end	

Así evitamos que nuestro método conozca la forma en la que queremos manejar la excepción, dándonos la oportunidad de realizar otra acción que nos ayude a recuperarnos del error, pudiendo usarlo de la siguiente manera:

fill_information(fields) do
  puts error.message
  # La excepción no se propagaria
end

o manejando la excepción a nuestro antojo:

fill_information(fields) do |field, error|
  field.failed = true
  send_failed_email
  ...
end

5. Uso del throw/catch

Finalmente algo que desconocía totalmente es el uso de throw/catch y la sutil pero importante diferencia respecto a raise. Se puede resumir en la intención de nuestro fallo, a diferencia de raise que propaga errores, throw propaga un evento sin consecuencias para la aplicación con la finalidad de terminar un proceso.

Podemos entender más su poder con el siguiente ejemplo:

def find_document(documente_name)
  current_user.document_list.each do |name|
    throw :found, name if document_name==name	
  end
end	

La intención es la de buscar un documento, tan pronto como lo hagamos regresar y terminar la ejecución de esa función ya que nuestro objetivo está completo, de esta forma quien mandó a llamar nuestro método puede esperar por el evento del archivo encontrado y realizar sus propias acciones, así:

def my_other_method(document_name)		
  document = catch(:found) do 
    find_document(documente_name)				
  end
  send_to_user(document)
end

Si te das cuenta, el uso es muy similar a un raise, pero la intención es totalmente distinta, aquí lo usamos como un mecanismo para notificar a quien esté interesado siendo una forma clara de terminar un proceso.

Conclusión

Es importante no dejar de crecer profesionalmente, una de las mejores maneras es el aprovechar la experiencia y consejos de personas con más conocimientos, en esta ocasión Avdi Grimm a través de su libro nos enseña cómo el código es una historia contada por nosotros, entre mas clara y ordenada sera mas facil de transmitir nuestras intenciones a sus lectores, nos muestra diferentes técnicas fáciles de usar en nuestras aplicaciones que ayuden con este propósito, de una forma que podamos explotar la simplicidad y expresividad que nos da ruby para resolver los problemas diarios.




comments powered by Disqus

Siguenos

Boletí de noticias