Ruby Modules II: Cómo usar módulos para añadir métodos de clase.

Este post muestra una técnica bastante útil que descubrí hace unos días.

Resulta que tenía media docena modelos con persistencia, y en la sección de administración de a web, pues tenía que listar todos y cada unos de ellos. Restaurants, Reviews, Users, Photos, Courses y Ticketlines. Y cuando listaba, tenía que filtrar la búsqueda usando unos parámetros y paginarla con will_paginate. Algo bastante típico.
Por tanto, mi AdminController tenía 6 acciones, una para listar cada tipo de modelo, casi idénticas, mas o menos así.

class AdminController < ActionController::Base
  protect_from_forgery
  layout 'index_layout'
  before_filter :redirect_unless_admin
  helper_method :sort_column,:sort_direction
  
  def users
    @models = User.where(build_exceptions)
                  .where(build_condition(User))
                  .order(sort_column + ' ' + sort_direction)
                  .paginate(:per_page=>8, :page=>check_page)
    respond_to do |format|
      format.html { render(:layout =>nil) if params[:no_layout] }
      format.js { render 'ajax_response' }
    end
  end
  # more actions => restaurants, reviews, photos ...
  
  private
  def redirect_unless_admin
    redirect_to(root_path,:notice=>t(:admins_only)) unless current_user && current_user.admin
  end  
  def build_condition(klass)
    # Returns a well formed string to use in WHERE. Uses params[:search]
  end
  def build_exceptions
    # Resturn a well formed string to user in where. Uses params[:search]
  end
  def check_page
    # Returns params[:page] satinized
  end  
  def sort_column
    # Returns params[:sort_column] sanitized
  end
  def sort_direction
    %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc'
  end
endd
end 

Dejando de lado los métodos auxiliares, que no tienen importancia, el problema radica en que todas las acciones eran idénticas a users, y el proceso de filtrar resultados, en este caso, era bastante complejo(de ahi que existe un método build_exceptions que no viene a cuento explicar), y repetitivo.
La primera opción para no hacer esto, es crear un sola acción, y enviar como parámetro el tipo de modelo sobre el que buscar. No me gustaba porque quería mantener las rutas como las tenía (/admin/users; /admin/restaurants; …). Además, me obligaba a crear otro saneador para que no me pasasen como parámetro un tipo de modelo erroneo que no existe.

La siguiente idea, que por cierto, es perfectamente válida, es mediante herencia.
Si todos los modelos van a poder filtrarse, puedo crear una clase intermedia entre ActiveRecord y mis modelos.

class FilterableModel < ActiveRecord::Base
  self.abstract_class = true
  
  def self.search(params)
    self.where(build_exceptions(params)).where(build_condition(params))
  end
  
  private
  def self.build_condition(params)
    # ...
  end
  def self.build_exceptions(params)
    # ...
  end
end

class User < FilterableModel
  # stuff
end

Esta solución debería funcionar (no lo he probado, lo he codificado al vuelo ahora mismo). Pero con esta aproximación, los métodos para sanear los parámetros todavía están en el controlador, y si además de listar restaurantes en AdminController los listo en otros controladores, tendría o bien que meterlos también dentro del la clase intermedia como métodos estáticos, pero no me siento cómodo metiendo en los modelos métodos que sanean los parámetros de las respuestas HTML, o bien que sacarlos para un helper y poder acceder a ellos desde cualquier controlador, que parece mucho más correcto.

Y aunque esta aproximación me parece válida, me gusta más la idea de implementar toda la lógica relacionada de alguna manera con las búsquedas en un módulo, y ya de paso, dejar el controlador lo mas limpio posible extrayendo los métodos que sanean los parámetros al helper AdminHelper.

# File: /app/helpers/admin_helper.rb
module AdminHelper  
  def handle_response
    # Handles response (layout or not, html/js, ect..)
  end
  def check_page
    # Returns params[:page] satinized
  end  
  def sort_column
    # Returns params[:sort_column] sanitized
  end
  def sort_direction
    %w[asc desc].include?(params[:direction]) ? params[:direction] : 'asc'
  end  
end
# File: filterable.rb 
# Yo la coloqué con los helpers, pero tal vez sea más correcto colocarla en /lib
module Filterable
  extend ActiveSupport::Concern
  
  module ClassMethods
    def search(search, column, direction)
      where(build_exceptions(search)).where(build_condition(search)).order(column ? "#{column} #{direction}" : "")
    end
    def build_condition(search)
      # ...
    end
    def build_exceptions(search)
      # ...
    end
  end
end

AdminHelper no tiene ninguna ciencia. Es un helper de toda la vida. Solo tenemos que acordarnos de incluirlo en el controlador.
El módule filterable hace uso de una novedad introducida en rails3. A partir de rails3, un módulo que extienda ActiveSupport::Concern , buscará dentro de sí mismo otro módulo anidado llamado ClassMethods. Todos los métodos incluidos en ClassMethods, se añadirán como métodos de clase a las clases que incluyan el módulo Filterable. Antes de rails3 se podía hacer, pero era mas complicado.
¿Qué hemos logrado? Pues esto:

# File: /app/models/user.rb
class User < ActiveRecord::Base
  include Filterable
  # ...  
end
# File: /app/controllers/admin_controller.rb
class AdminController < ActionController::Base
  include AdminHelper
  protect_from_forgery
  before_filter :redirect_unless_admin
  
  def users
    @models = User.search(params[:search],sort_column,sort_direction)
                  .paginate(:per_page=>8,:page=>check_page)    handle_response
  end
  # Methods => restaurants, reviews, courses ...

  private
  def redirect_unless_admin
    redirect_to(root_path,:notice=>t(:admins_only)) unless current_user && current_user.admin
  end

end

Al incluir Filterable en las clases del modelo, podemos hacer User.search(..params..) igual que si fuese un scope como where o order.
De esta manera conseguimos unos controladores más limpios.
El hecho de extraer los metodos auxiliares a un helper y incluirlo en el controlador, es secundario, pero creo que también clarifica el código y puede que en otro controlador necesitemos esos mismos métodos para sanear parámetros, y así los incluimos facilmente.

Y con esto, ya si, hasta la semana que viene 🙂

Anuncios

, , , , , , , , ,

  1. Deja un comentario

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: