homeASCIIcasts

259: Decent Exposure 

(view original Railscast)

Other translations: En Ja

Other formats:

Written by Juan Lupión

En este episodio veremos una gema llamada decent_exposure. Se trata de una sencilla gema que implementa un concepto muy elegante: crear una interfaz de métodos en el controlador a los que puede acceder la vista en lugar de utilizar variables de instancia. Dicho interfaz se define con un método llamado expose.

Tratemos primero de aplicar este concepto de forma manual antes de empezar con decent_exposure. La aplicación con la que vamos a trabajar es un blog muy sencillo con muchos Articles, cada uno de los cuales puede tener muchos Comments.

Nuestra aplicación de blog.

El código de ArticlesController es el habitual para cualquier controlador. Por ejemplo la acción index crea una variable de instancia llamada @articles.

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
    @articles = Article.order(:name)
  end

  # Se omiten las otras acciones
end

@articles se usa luego en el código de la vista index para recorrer los artículos y mostrarlos de uno en uno.

La primera vez que uno usa Rails puede parecer extraño que las variables de instancia de un controlador se encuentren compartidas con las vistas, dado que por lo general las variables de instancia son privadas a una clase. Veamos a continuación un enfoque alternativo que comparte los datos exponiendo métodos en los controladores que devuelven o bien instancias de los modelos o bien listas de instancias.

Empezaremos quitando la línea de código que recupera los artículos en la acción index y la pondremos en un método llamado articles. Queremos que se utilice @articles como si fuese una caché, de forma que se consulten los articulos una sóla vez. Para esto podemos usar el operador ||=, con lo cual tanto el controlador como en las vistas debemos dejar de hacer referencia a la variable de instancia. Haremos que articles sea una función helper para que pueda ser usada desde las vistas.

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
  end

  private
  def articles
    @articles ||= Article.order(:name)
  end
  helper_method :articles
end

Ahora en la vista ya podemos reemplazar la llamada a la variable de instancia por una llamada al nuevo método articles.

/app/views/articles/index.html.erb

<% title "Articles" %>

<div id="articles">
<% for article in articles %>
  <h2>
    <%= link_to article.name, article %>
    <span class="comments">(<%= pluralize(article.comments.size, 'comment') %>)</span>
  </h2>
  <div class="created_at">on <%= article.created_at.strftime('%b %d, %Y') %></div>
  <div class="content"><%= simple_format(article.content) %></div>
<% end %>
</div>

<p><%= link_to "New Article", new_article_path %></p>

Las otras acciones del controlador encuentran o crean un único artículo, por lo que podemos hacer algo parecido definiendo un método llamado article. Dicho método será un poco más complejo que articles porque tiene que hacer cosas distintas según qué parámetros reciba. Este método quedaría así:

/app/controllers/articles_controller.rb

def article
  @article ||= params[:id] ? Article.find(params[:id]) : Article.new(params[:article])
end
helper_method :article

Si el parámetro id está presente el método recuperará el Article que tenga dicho id. Si no, creará uno nuevo utilizando lo que hayamos recibido por params[:article]. Con esto ya tenemos un método que podemos usar como alternativa a la variable de instancia @article, así que podemos eliminar las líneas que manipulaban dicha variable en las acciones.

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  def index
  end

  def show
  end

  def new
  end
  
  def create
    if article.save
      redirect_to articles_path, :notice => "Successfully created article."
    else
      render :new
    end
  end

  def edit
  end
  
  def update
    if article.update_attributes(params[:articles])
      redirect_to articles_path, :notice => "Successfully updated article."
    else
      render :edit
    end
  end
  
  def destroy
    article.destroy
    redirect_to articles_url, :notice => "Successfully destroyed article."
  end

private
  def articles
    @articles ||= Article.order(:name)
  end
  helper_method :articles
  
  def article
    @article ||= params[:id] ? Article.find(params[:id]) : Article.new(params[:article])
  end
  helper_method :article
end

Algunas de las acciones se han quedado sin código alguno porque no hacían nada que no fuese definir una variable de instancia, que es lo que estamos ahora gestionando en los métodos privados que hemos escrito. Aún tenemos que ir a las vistas de ArticleController y cambiar el uso de estas variables de instancia por una llamada al método apropiado. Por ejemplo, la vista show deberá quedar así:

/app/views/articles/show.html.erb

<% title article.name %>

<%= simple_format article.content %>

<p>
  <%= link_to pluralize(article.comments.size, 'Comment'), [article, :comments]%> |
  <%= link_to "Back to Articles", articles_path %> |
  <%= link_to "Edit", edit_article_path(article) %> |
  <%= link_to "Destroy", article, :method => :delete, :confirm => "Are you sure?" %>
</p>

No mostramos las otras vistas pero habría que cambiarlas de la misma manera.

Otra ventaja de este enfoque es que hace una carga diferida. Si añadiésemos caché de acciones a, por ejemplo, la acción show, el artículo mostrado sólo sería recuperado de la bae de datos si fuésemos a mostrar la vista dado que no se hace nada con el artículo en la capa del controlador. La caché de acciones funcionaría muy bien aquí porque la acción no se ejecuta a no ser que el controlador realmente lo necesite.

decent_exposure en acción

La solución que ya tenemos es razonablemente buena pero sería aún mejor tener una forma más cómoda de definir los métodos que exponemos en las vistas, y aquí es donde entra en juego decent_exposure. Su método expose se puede usar para definir métodos que expongan los modelos a la vista de forma similar a los métodos articles y article que hemos escrito más arriba. El método expose por defecto tiene un funcionamiento que nos es útil: buscará un modelo por su parámetro id y si dicho parámetro no está presente creará un modelo nuevo utilizando los parámetros correspondientes que pueda localizar. Esto quiere decir que se pueden usar estos métodos para buscar o crear modelos sencillos. Para comportamientos más sofisticados tenemos que pasar un bloque al método para definirlo. Además decent_exposure se encargará de gestionar la caché por nostros.

Incorporemos la gema en nuestra aplicación. Primero añadiremos la gema al Gemfile y luego ejecutaremos el comando bundle.

/Gemfile

source 'http://rubygems.org'

gem 'rails', '3.0.5'
gem 'sqlite3'
gem 'nifty-generators'
gem 'decent_exposure'

Ahora en el controlador ArticlesController podemos cambiar los métodos article y articles por dos llamadas a expose.

/app/controllers/articles_controller.rb

class ArticlesController < ApplicationController

  expose(:article)
  expose(:articles) { Article.order(:name) }
  
  def index
  end
  
  # Se omiten las otras acciones
end

El comportamiento definido por defecto es justo lo que queremos para un único Article pero para el caso del listado de artículos hemos copiado el cuerpo del método articles de antes y lo hemos puesto en el bloque del método expose.

Si ahora volvemos a cargar la aplicación funcionará igual que antes pero ahora los controladores son mucho más limpios porque están usando los métodos proporcionados por decent_exposure en lugar de usar variables de instancia.

Recursos anidados

¿Cómo se gestionan con decent_exposure los recursos anidados, como por ejemplo los comentarios en nuestra aplicación? Los comentarios están anidados dentro de los artículos.

/config/routes.rb

Blog::Application.routes.draw do
  root :to => "articles#index"

  resources :articles do
    resources :comments
  end
end

Este es el aspecto que tiene el controlador CommentsController:

/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  def index
    @article = article.find(params[:article_id])
    @comments = @article.comments
    @comment = Comment.new
  end
  
  def new
    @article = Article.find(params[:article_id])
    @comment = @article.comments.build
  end
  
  def create
    @article = Article.find(params[:article_id])
    @comment = @article.comments.build(params[:comment])
    if @comment.save
      redirect_to @comment.article, :notice => "Successfully created comment!"
    else
      render :new
    end
  end
end

Aquí todavía nos encontramos usando variables de instancia. Al principio de cada acción recuperamos un artículo y luego construimos un comentario para dicho artículo. También podemos emplear decent_exposure en este caso porque la gema soporta perfectamente el uso de recursos anidados.

Al igual que antes vamos a cambiar cada variable de instancia por una llamada a expose. Podemos utilizar el comportamiento por defecto para recuperar un Article y su Comment pero para recuperar el listado de comentarios tendremos que definir ese comportamiento, tras lo cual podemos borrar las líneas del controlador que asignan las variables de instancia y en el código que quede cambiar dichas variables por llamadas a los métodos apropiados. Con todos estos cambios, el controlador queda mucho más claro:

/app/controllers/comments_controller.rb

class CommentsController < ApplicationController
  expose(:article)
  expose(:comments) { article.comments }
  expose(:comment)
  
  def index
  end
  
  def new
  end
  
  def create
    if comment.save
      redirect_to comment.article, :notice => "Successfully created comment!"
    else
      render :new
    end
  end
end

Al igual que hicimos cuando cambiamos el código de ArticlesController ahora nos toca actualizar las vistas relacionadas con este controlador para que invoquen a los métodos que han sido generados por decent_exposure en lugar de las variables de instancia, por ejemplo el siguiente parcial de formulario:

/app/views/comments/_form.html.erb

<%= form_for [article, comment] do |f| %>
  <%= f.error_messages %>
  <%= f.hidden_field :article_id %>
  <p>
    <%= f.label :name %>
    <%= f.text_field :name %>
  </p>
  <p>
    <%= f.label :content, "Comment" %><br />
    <%= f.text_area :content, :rows => 12, :cols => 35 %>
  </p>
  <p><%= f.submit %></p>
<% end %>

Si volvemos a probar la aplicación veremos que sigue funcionando igual que antes pero el código de los controladores está mucho mejor.

Tenemos que tener en cuenta un potencial peligro con el uso de decent_exposure. Cuando usamos expose con su comportamiento por defecto (recuperar un único modelo) buscará una versión pluralizada del nombre que recibe (por ejemplo :article) e intentará recuperar y construir registros a través de ese ámbito si es que existe. Por ejemplo, supongamos que tenemos las siguientes dos llamadas a expose en ArticlesController.

/app/controllers/articles_controller.rb

expose(:article)
expose(:articles) { Article.order(:name).where(:visible => true) }

Cualquier llamado al método en singular intentará recuperar el artículo basándose en un ámbito articles en plural. Por tanto, dado el código anterior, cuando busquemos un único artículo éste sólo será devuelto si dicho articulo es visible. Para soslayar este comportamiento tenemos que cambiar el nombre de la versión en plural para que tenga un nombre más descriptivo. En este caso, lo cambiaremos por visible_articles.

/app/controllers/articles_controller.rb

expose(:article)
expose(:visible_articles) { Article.order(:name).where(:visible => true) }

Con esto la segunda llamada a expose no será tomada como el ámbito base por defecto sobre el que se construyen las acciones. Con este cambio, por supuesto, tendremos que cambiar las llamadas en las vistas.

Modificación del comportamiento por defecto

Si alguna vez tenemos que cambiar el comportamiento por defecto del método expose podemos emplear el método default_exposure pasándole un bloque: el comportamiento que definamos en dicho bloque sobreescribirá al comportamiento por defecto. El nombre recibido por expose será propagado al bloque de default_exposure.

class MyController < ApplicationController
  default_exposure do |name|
    ObjectCache.load(name.to_s)
  end
end

Por lo general no nos hará falta sobreescribir el comportamiento por defecto, pero ahí queda esta opción por si acaso alguna vez lo necesitamos.

Con esto cerramos este episodio. decent_exposure es una solución muy limpia para reorganizar nuestros controladores y merece la pena tenerla en cuenta si pensamos que puede encajar con nuestra forma de trabajar.