Refactoring de “Fat Methods” – Episodio 5

Pasar programadores PHP a Ruby no siempre es una tarea trivial, mayormente porque lo primero que tratan de hacer es escribir código en lugar de pensar que es lo que quieren hacer :). No es que me queje, a todos nos pasa que resolvemos el problema como primero sale (más aún cuando las fechas de entrega son apretadas :).

El otro día queriendo modificar un código me encuentro con este before_filter en un controller :

def setprofile
  @viewer = false
  if (!params['id'] && !params['profile_id'])
    @profile = @passport
  else
    if params['profile_id']
      pid = params['profile_id']
    else
      pid = params['id']
    end
    @profile = Passport.find_by_id(pid)			
    if (!@profile) 
      redirect_to(:controller => 'my3dg', :action => 'show') and return false
    end
    if (@profile.id != @passport.id)
      @viewer = true
    end			
  end
end

Lo primero que pense “uff, que paja, que complicado” hasta que miré mejor la estructura y ahí comprendí que en realidad era muy simple, solo que estaba escrito de forma complicada. Lo primero que me gustó de Ruby cuando empecé a entenderlo fue el uso de “||” para reemplazar esos molestos if anidados cuando tengo más de una opción. “Si no está a, poner a en el valor default”. En ruby es un simple “a || valor_default” (ojo con los valores de verdad en ruby).

Lo otro que veo mucho (incluso en los trabajos que corrijo en la facultad) es utilizar un if para después setear una variable en true o false, cuando casi todos los lenguajes aceptan expresiones booleanas del lado derecho de la igualdad.

Refactorizando el método anterior obtenemos el siguiente código :

def setprofile
  @profile = Passport.find_by_id(params['profile_id'] || params['id']) || @passport
  @viewer = @profile.id != @passport.id
  redirect_to(:controller => 'my3dg', :action => 'show') unless @profile
  return false unless @profile
end

Como nota, uso find_by_id porque retorna nil en caso de no encontrar el registro, en cambio find tira una excepción RecordNotFound, de esa forma puedo acoplar otro “||” para setear el valor default (@passport es la credencial del usuario loggeado en este caso).

Refactoring de “Fat Methods” – Episodio 4

En la última entrega habíamos logrado separar las notificaciones del controlador. Una de las cosas que todavía me seguían molestando eran los métodos donde obtengo las direcciones de email, como por ejemplo :

@notifications = @match.players.select {|p| p.user.notify_new_match_comment? }.collect {|p| p.user.email}

Lo que busca esto es obtener los emails de los usuarios para notificar ante un evento dado (un nuevo partido creado, un nuevo comentario, una petición de amistad, etc). Me molesta principalmente porque se ven horribles mis controllers :).

La respuesta que motiva este episodio llegó por casualidad. Intentando solucionar un bug que tenía en uno de los plugins que uso (has_many_friends, que es feo pero cómodo para un proyecto como este de prueba) vi algo que no conocía : las asociaciones (has_many, has_one, etc) aceptan un bloque donde podemos agregar funcionalidad extra.

Entonces, por ejemplo, si yo quisiera que el ejemplo anterior pueda ser escrito como @match.players.notificables (si, el nombre no es de lo mejor, pero es a modo ilustrativo) simplemente debería agregar ese método como sigue :

class Match < ActiveRecord::Base
  has_many :players  do
    def notificables
      collect {|p| p.user.email if !p.user.email.blank? && p.user.notify_new_match_comment? }
    end
  end
end

# Ejemplo de uso
@notifications = @match.players.notificables

El método que agregamos trabaja directamente sobre la asociación, que en este caso es una colección de Player, por lo que el collect nos dará todos los emails de los usuarios que se anotaron al partido y tienen activa la opción de recibir notificaciones cuando alguien deja un comentario nuevo en ese partido.

Los métodos que agregamos pueden aceptar parámetros también, con lo cual podría aceptar un symbol con el tipo de permiso a verificar y tener todos los emails de los usuarios para un tipo de notificación dada. Ahora si se hace un refactor de las opciones de notificación de los usuarios tenemos centralizada la lógica en los modelos en lugar de tenerla en los controladores u observers.

Delegando atributos de ActiveRecord

El otro día tratando de mejorar un modelo de una aplicación me topé con el problema de tener muchos atributos virtuales para poder acceder a atributos de un objeto relacionado y no caer en lo siguiente :

class Player < ActiveRecord::Base
  belongs_to :user

  def name
    user.name
  end

  def email
    user.email
  end

  # ... y varios más
end

Esto no lo puedo evitar mucho ya que Player es una clase intermedia en una relación de N-M entre los usuarios y los partidos, y además contiene información necesaria para la lógica del modelo.

Tampoco era muy feliz tener por todos lados Message Chains:

@player.user.name
@player.user.email
# etc ....

Una de las recomendaciones que se usan en estos casos es Hide Delegate para ocultarle al cliente de donde sale el dato realmente. Cabe aclarar que no siempre son un problema las llamadas encadenadas.

Recordando el anuncio de Rails 2.2 noté que al final hablaba de un delegate que tenía un nuevo feature. Buscando un rato por google encontré este post donde hablaba de un método delegate para hacer justamente esto que yo quería.

Lo extraño, que también menciona el autor es que no está documentado en la API oficial de Rails aunque mirando el código veo que explica como usarla. Explícitamente dice :

Provides a delegate class method to easily expose contained objects’ methods as your own

En la documentación también aclara que es útil tanto para atributos propios como para asociaciones entre diferentes instancias de ActiveRecord. La realidad es que por cómo está implementada funciona para cualquier objeto ruby que se nos ocurra.

El ejemplo con el que empezamos el post quedaría resumido a :

class Player  :user
end

Quedando mucho más corto el código, por lo tanto más fácil de mantener.

Tomando prestado los ejemplos del otro blog, también podemos hacer cosas con atributos que no son asociaciones, como por ejemplo una fecha :

# Forma abreviada
class Content  :published_at
end

# Forma desglosada
class Content  :published_at
  delegate :month, :to => :published_at
  delegate :day, :to => :published_at
end

# Podemos escribir
@content.year

# en lugar de
@content.published_at.year

Refactoring de “Fat Methods” – Episodio 3

En el Episodio 1 trabajé el método Matches#create y había dejado una parte a la que Des preguntó por qué la ignoraba. En ese momento no quería extender mucho más el post por lo que en esta oportunidad vamos a completar el refactoring pendiente.

El código que habíamos logrado en ese momento es el siguiente:

  def create
    @match = current_user.matches.create!(params[:match])

    @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&email)
    Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

    flash[:success] = "El partido fue creado."
    redirect_to matches_path
  rescue
    render :action => 'new'
  end

Lo que nos quedaba terminar era cómo mejorar el envío de las notificaciones para aquellos amigos que tenían activa dicha opción. Como primer paso vamos a separar la lógica del mailer de manera que no quede mezclado en el controller, y ya que estamos, que no importa donde se cree un partido el email salga igual sin la necesidad de copiar y pegar código.

Una opción es usar un callback after_create como ya hicimos en el post pasado, pero a mi no me gusta mezclar en los modelos lógica que no tiene que ver con la persistencia. La razón es que esta tarea de enviar emails no es algo propio de un modelo, no debería tener esa responsabilidad. Pero dejarla en el controller sería “irresponsable” :P.

Una opción, no muy acertada, podría ser usa un filtro que se ejecute después del create (after_filter) pero quizás no siempre tendríamos la posibilidad de ejecutar un filtro. La mejor opción es utilizar un Observer para mirar al modelo Match, y cuando uno nuevo es creado, ejecutar nuestro código.

Los Observers se registran a si mismos a un modelo dado escuchando los callbacks que nosotros definamos. Estos callbacks son los mismos que existen en ActiveRecord, siendo algunos : after_create, before_create, after_save, etc…

Como vemos, el Observer es casi lo mismo (a nivel práctico claro está) que usar los callbacks de ActiveRecord pero sin tener que mezclar acciones que no son propias de los modelos. Muchas veces uno se olvida que existen ya que generalmente se los utiliza para las estrategias de invalidación de cache (que puntualmente estos observers se llaman Sweepers).

El código del observer quedaría ahora :

  # app/model/match_observer.rb
  class MatchObserver < ActiveRecord::Observer
    def after_create(contact)
      @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
      Emailer.deliver_match_created(@match, @notifications) if @notifications.any?
    end
 end

Como se ve, en ningún lugar hacemos referencia al modelo observado. Esto es porque Rails lo infiere automáticamente a partir del nombre del Observer. Claro está que si por alguna razón usamos un nombre que no permita inferirlo lo podemos especificar.

Lo único que faltaría es decirle a Rails que existe el Observer, para que lo cargue y lo registre con la clase correspondiente. Para eso debemos agregar en nuestros environment lo siguiente :

  config.active_record.observers = :match_observer

Ahora si, veamos como quedó nuestro controlador totalmente refactorizado :

  # Dentro de matches_controllers.rb
  def create
    @match = current_user.matches.create!(params[:match])
    flash[:success] = "El partido fue creado."

    redirect_to matches_path
  rescue
    render :action => 'new'
  end

Ahora si nuestro controlador está bastante mejor. Para la próxima entrega vamos a como mejorar las asociaciones de los modelos para poder cambiar el match.owner.friends.select que no es para nada prolijo.

Refactoring de “Fat Methods” – Episodio 2

Continuando con esta serie de ejemplos de como refactorizar métodos de controladores vamos a seguir ahora con uno que cuando lo volví a ver apestaba feo feo.

El método en cuestión tiene por objetivo que el usuario actual se suscriba (es decir, anote) para jugar un partido determinado. Para eso el siguiente código es el que está siendo ejecutado actualmente :

  # app/controllers/matches_controllers.rb
  def subscribe
    flash[:notice] = "Ya formás parte del equipo!" and 
        redirect_to match_path(@match) and 
        return if @match.has_player? current_user

    flash[:error] = "El equipo está completo." 
      and redirect_to match_path(@match) and ]
      return if @match.available_places(params[:team].to_i)  current_user, :team => params[:team]

    redirect_to match_path(@match)
  end

Lo feo del problema se puede ver desde dos perspectivas : una que es una excusa y otra que es la razón de la desprolijidad. La excusa para este código tan horrible es que en caso de no poder agregar el jugador al equipo elegido tengo que mostrar un mensaje de error y redireccionar. Pero el redirect_to lo que hace es setear un header nada más, es decir que no hace un return auto-mágico, y de no hacerlo yo el segundo redirect_to (ubicado al final del código) haría que se lance una Exception por haber reenviado los headers.

El problema real es que estoy delegando en el controlador la responsabilidad de la clase Player para determinar si es válido crear una instancia para un partido y usuario dado.

Es por eso que lo primero que debemos hacer es usar los validators de Rails para esta tarea :

  # app/models/player.rb
  def validate
    errors.add_to_base("Ya formás parte del equipo!") if match.has_player? user
    errors.add_to_base("El equipo está completo.") if match.available_places(team) < = 0
  end

En este caso los errores no dependen de un atributo, por lo que usamos add_to_base para que los errores digan lo que queremos. Este método validate es llamado por Rails al crear o actualizar una instancia de Player, y si hay algún error nunca llega a la DB.

Habiendo quitado la validación del controlador ahora podemos escribirlo de una forma más prolija y entendible :

  # app/controllers/matches_controllers.rb
  def subscribe
    @player = @match.players.create :user => current_user, :team => params[:team].to_i

    flash[:notice] = @player.errors.full_messages.join(", ") if @player.errors.any?

    redirect_to match_path(@match)
  end

Lovely :). Se crea un Player para el Match, se prepara una alerta visual en caso de que haya algún error y luego se redirige a la página del partido. Si ven el antes y el después creo que nadie me va a negar de la mejora :).

Por hoy es todo, hasta la próxima entrega!.

Refactoring de “Fat Methods” – Episodio 1

Luego de escribir mucho código uno comienza a mirar a ver que hacen los de al lado, y al mirar se da cuenta de lo mal que uno escribe código cuando comienza con un nuevo lenguaje :). Algo de eso me pasa hoy en día con ¡Falta Uno!.

En el camino de emprolijar las cosas para liberar el código tuve la necesita de poner a refactorizar el código porque hay partes que no son nada bonito. Voy a tratar de plasmar en los siguientes posts algunas lecciones aprendidas junto con el paso a paso del refactoring del código.

Antes que nada y como buena práctica es ideal tener buenos test sobre los que se va a refactorizar ya que es muy fácil romper alguna funcionalidad que se suponía que andaba. Si bien para los ejemplos acá voy a ignorar esta buena práctica, no se los aconsejo :).

Vamos a empezar con el método create de controlador Matches, encargado de crear un nuevo partido :

  def create
    @match = current_user.matches.create(params[:match])
    if @match.errors.empty?
      Player.create :match => @match, :user => current_user

      @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
      Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

      flash[:success] = "El partido fue creado."
      redirect_to matches_path
    else
      render :action => 'new'
    end
  rescue
    render :action => 'new'
  end

Asusta un poco, ¿no?. Para entender el problema, veamos primero como que involucra crear un nuevo partido :

  • Crear el partido
  • Anotar al usuario que creó el partido en uno de los equipos
  • Notificar a todos los amigos que se creó un nuevo partido

Dado esto, pasemos a analizar los problemas. La primer pregunta que habría que hacerse es : ¿Quién es el responsable de anotar al jugador?, ¿Tiene sentido que un usuario pueda crear un partido sin que esté anotado?. En el contexto nuestro, la respuesta a la última pregunta es “No”, solo porque así lo defino yo :). Entonces podemos responder a la primer pregunta : El Match debería ser el responsable de anotar al dueño del partido cuando es creado.

Para lograr esto último lo que debemos hacer es mover la lógica de creación del Player dentro de un callback en el modelo Match, quedando algo así :

class Match  self, :user => owner
  end
end

De esta forma nos aseguramos que se cumpla lo pedido sin correr el riesgo de que se puedan crear matches sin que el owner este anotado inicialmente. También ganaríamos en el caso de que exista alguna otra forma de crear un partido, por ejemplo si agregáramos la posibilidad de crear un partido enviando un email, no tendríamos la necesidad de repetir la lógica inicial en dos lugares diferentes.

Y ahora nuestro controlador queda algo cómo :

  def create
    @match = current_user.matches.create(params[:match])
    if @match.errors.empty?
      @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
      Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

      flash[:success] = "El partido fue creado."
      redirect_to matches_path
    else
      render :action => 'new'
    end
  rescue
    render :action => 'new'
  end

Lo siguiente es limpiar un poco lo lógica del modelo. Una cosa que molesta a la vista son las dos llamadas a render, ¿son realmente necesarias? La respuesta en NO. Con el rescue lo que hacemos es que dada una Exception vuelva al formulario de creación, lo mismo que si el partido no es válido, por lo que simplemente podríamos haber usado el método create! que lanza una Exception si el modelo no es válido evitándonos así el molesto if. Quedando ahora :

  def create
    @match = current_user.matches.create!(params[:match])

    @notifications = @match.owner.friends.select {|f| f.notify_new_matches? }.collect(&:email)
    Emailer.deliver_match_created(@match, @notifications) if @notifications.any?

    flash[:success] = "El partido fue creado."
    redirect_to matches_path
  rescue
    render :action => 'new'
  end

Que si lo comparan con la versión inicial es mucho más legible. “Fat model, skinny controllers” es la clave a seguir. Todavía queda por mejorar la forma en que se genera la lista de emails de los amigos que desean recibir la notificación, pero no es algo que me moleste de momento.

En otras entregas seguiré mostrando el resto del refactoring de otro métodos que son aún peores que este :).