Usando presenters no Rails
Um problema muito recorrente de aplicativos de médio-grande porte é que as views são uma bagunça.
É muito comum termos condições em nossas views. Objetos possuem diferentes estados e muitas vezes precisamos mostrar esses estados visualmente. E normalmente começamos pelo caminho mais fácil, que é adicionar ifs na view.
Acontece que isso não precisa ser sempre assim. Neste artigo vou mostrar como funciona um pattern chamado Presenter, que permite diminuir/remover a complexidade de views e controllers.
Começando com o seu próprio presenter
A primeira coisa que você precisa detectar é que tipo de lógica é possível extrair de sua view. Algumas coisas mais genéricas fazem mais sentido serem extraídas como helpers. Outras, como ifs para determinar qual partial deve ser renderizada provavelmente devem ser movidas para seu presenter.
Imagine que você tenha uma view como esta:
<h1><%= @product.name %></h1>
<% if @product.description %>
<p class="description"><%= @product.more %></p>
<% end %>Não se deixe enganar por esse tipo de lógica. Embora pareça inofensiva, coisas como esta podem sair do controle rapidamente.
Esta view precisa de uma variável @product que deve ser definida em nosso controller:
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
end
endAgora, precisamos de uma classe que irá “envelopar” nossa instância da classe Product. Crie o arquivo app/presenters/product_presenter.rb. Como este diretório não está no load path do Rails, vamos ter que fazer uma configuração no arquivo config/application.rb.
module HOWTO
class Application < Rails::Application
# ...
config.autoload_paths << config.root.join("app/presenters").to_s
end
endVolte ao arquivo product_presenter.rb e crie a classe ProductPresenter.
class ProductPresenter
def initialize(product)
@product = product
end
endEsta classe será responsável por expor todos os atributos que nossa view irá acessar. Em vez de definir cada um dos métodos manualmente, podemos apenas delegar as chamadas para o objeto. Para fazer isso, vamos usar o método Module#delegate, adicionado pelo ActiveSupport.
class ProductPresenter
delegate :name, :description, to: :"@product"
def initialize(product)
@product = product
end
endQualquer chamada aos métodos ProductPresenter#name e ProductPresenter#description serão delegadas para o objeto que foi armazenado em @product. Você também poderia utilizar o módulo Forwardable, mas a versão adicionada pelo ActiveSupport é mais elegante.
Agora, podemos remover aquele if. Se você não se lembra mais dele, dê uma última olhadela, pois logo ele não mais existirá! Adicione o método description. Faça com que este método retorne o parágrafo com a descrição, caso ela tenha sido definida. Como é necessário retornar uma tag HTML, vamos usar o helper content_tag.
class ProductPresenter
delegate :name, to: :"@product"
def initialize(product)
@product = product
end
def description
if @product.description.present?
helpers.content_tag(:p, @product.description, class: "description")
end
end
private
def helpers
ApplicationController.helpers
end
endAltere o controller para que ele passe a instância da classe Product para o presenter.
class ProductsController < ApplicationController
def show
@product = ProductPresenter.new(Product.find(params[:id]))
end
endPara finalizar, basta modificar nossa view.
<h1><%= @product.name %></h1>
<%= @product.description %>Para o caso de partials, o funcionamento é basicamente o mesmo. No entanto, em vez de fazer a renderização no próprio presenter, é mais fácil retornar o nome da partial que deve ser renderizada.
Imagine que nossa view tenha mais um if que irá renderizar uma partial diferente para produtos gratuitos.
<% if @product.paid? %>
<%= render "order", product: @product %>
<% else %>
<%= render "download", product: @product %>
<% end %>Podemos implementar um método chamado ProductPresenter#checkout_partial que irá fazer aquele if, retornando apenas o nome da partial.
class ProductPresenter
delegate :name, to: :"@product"
def initialize(product)
@product = product
end
def description
if @product.description.present?
helpers.content_tag(:p, @product.description, class: "description")
end
end
def checkout_partial
@product.paid? ? "order" : "download"
end
private
def helpers
ApplicationController.helpers
end
endE na nossa view, basta renderizar o retorno do método ProductPresenter#checkout_partial.
<%= render @product.checkout_partial, product: @product %>A esta altura, você já deve ter percebido como presenters podem remover completamente a lógica das views. Embora seja muito fácil fazer isso sem a necessidade de bibliotecas, algumas coisas precisam ser implementadas toda vez. É o caso de helpers, rotas e métodos de internacionalização.
Pensando nisso, decidi extrair aquela organização de código que eu estava utilizando em uma gem chamada simple_presenter.
Usando o simple_presenter
Para instalar, basta executar o comando abaixo:
$ gem install simple_presenterLembre-se de adicionar a gem ao arquivo Gemfile.
source :rubygems
gem "rails", "3.1.3"
gem "simple_presenter", "~> 0.1"Aquele mesmo presenter que definimos pode ser trocado por algo como isto:
class ProductPresenter < Presenter
expose :name, :description
def description
if @subject.description.present?
h.content_tag(:p, @subject.description, class: "description")
end
end
def checkout_partial
@subject.paid? ? "order" : "download"
end
endNote que não precisamos mais definir o método ProductPresenter#initialize, nem o método ProductPresenter#helpers. Também tivemos que mudar todas as referências a @product para @subject; isso é necessário porque, por padrão, o nome do objeto que receberá os métodos delegados é @subject.
O simple_presenter adiciona os métodos helpers e h que permite acessar os helpers do Rails. Os helpers de rotas podem ser acessados com os métodos routes e r. E, finalmente, os helpers de internacionalização podem ser acessados por translate e t, e localize e l.
Escrevendo testes
Escrever testes para presenters é muito simples. No caso do RSpec, basta criar o diretório spec/presenters. Aquele nosso presenter pode ter testes como este:
require "spec_helper"
describe ProductPresenter do
let(:product) { mock(Product, name: "Some product") }
subject { described_class.new(product) }
its(:name) { should eql("Some product") }
describe "#description" do
it "returns content" do
product.stub description: "Some description"
expected = %[<p class="description">Some description</p>]
subject.description.should eql(expected)
end
it "returns no message" do
subject.description.should be_blank
end
end
describe "#checkout_partial" do
it "returns partial for paid products" do
product.stub paid?: true
subject.checkout_partial.should eql("order")
end
it "returns partial for free products" do
product.stub paid?: false
subject.checkout_partial.should eql("download")
end
end
endFinalizando
Você deve ter percebido que presenters permitem tornar suas views muito mais simples. Além disso, eles tem a vantagem de serem fáceis de testar.
A coisa mais difícil dos presenters é se acostumar com eles. Mas depois que você se acostuma, dificilmente terá uma view complicada e também nem vai querer deixar de usá-los!
UPDATE 1: O Valim me lembrou que à partir do Rails 3 não é mais preciso adicionar diretórios app/*, pois o Rails faz isso automaticamente. Eu sabia disso, mas o costume ainda permanece.