#259 Decent Exposure
- Download:
- source codeProject Files in Zip (97.3 KB)
- mp4Full Size H.264 Video (21.4 MB)
- m4vSmaller H.264 Video (12.8 MB)
- webmFull Size VP8 Video (29.3 MB)
- ogvFull Size Theora Video (28.9 MB)
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
.
El código de ArticlesController
es el habitual para cualquier controlador. Por ejemplo la acción index
crea una variable de instancia llamada @articles
.
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.
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
.
<% 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í:
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.
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í:
<% 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
.
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
.
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.
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
:
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:
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:
<%= 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
.
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
.
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.