Usando presenters no Rails
18/12/11
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
end
Agora, 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
end
Volte ao arquivo product_presenter.rb e crie a classe ProductPresenter.
class ProductPresenter
def initialize(product)
@product = product
end
end
Esta 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
end
Qualquer 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
end
Altere 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
end
Para 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
end
E 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_presenter
Lembre-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
end
Note 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
end
Finalizando
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.
- Permalink
- Trackback
- Comentários (31)
- Ao som de: Less Than Jake – Dukes Of Hazzard
Textos escritos por
Comentários #
Presenter novo pattern?? Isso é Strategies!!!
Oi Nando,
Ainda não estou certo se gosto dessa idéia... Se aquele if que você colocou tivesse um pouco mais de html para ser renderizado, você não acha que pode acabar com uns htmls estranhos dentro das classes? Não me parece natural ter que mexer numa classe para alterar uma lógica de apresentação, apesar que parece ser esse é o objetivo desse estilo :). Tem algum exemplo mais tenso?
Falou,
Alberto
Acho que description não precisa ser mais delegado já que ele foi implementado no presenter, certo?
Ronaldo, quem falou que é um novo pattern?
Rafael, yep. Você está certo. Vou atualizar os exemplos. :)
Alberto, nesse exemplo, que é apenas uma tag, não me incomoda. Mais que isso, eu extrairia para uma partial ou faria a renderização da partial no presenter.
Essa linha daqui está certa?
h.content_tag(:p, @subject.description, class: "description")
Observe o "class:" aí acima...não deveria ser ":class =>" ou algo assim? JS tomou conta? =P
Fala Nando!
Como o Alberto falou, pode acontecer de termos muito HTML dentro da classe. Além do uso de partials que você sugeriu, pode-se utilizar o Cells também. Eu costumo usar presenters quando a lógica envolve apenas buscar coleções, manipular de alguma forma o objeto e suas associações afim de gerar uma string que será exibida, etc. Quando envolve muito HTML ai crio uma cell. Juntando o seu esquema de gerar o nome do partial a ser usado com os presenters pode-se chegar à uma solucão mais simples do que a oferecida pelo Cells. O que eu gosto dele é o esquema de herança e a organização dos arquivos dentro de pastas, além também de ter acesso a algumas coisas disponíveis em controllers.
O que você acha desse tipo de abordagem?
Concordo com Cássio. Utilizo da mesma forma, apenas para tratar coleções e evitar sujar o modelo. O que é html acho que é responsabilidade do ActionView tratar.
Leandro,
Essa sintaxe que o Nando usou é novidade no Ruby. Está certo!
Eu não curti muito mas existe e está certa.
Atualmente utilizo esse approach nos meus projetos: https://gist.github.com/1494843
Leandro, tá certa sim. É a sintaxe de hash do Ruby 1.9. :)
Cássio, confesso que nunca fui além do README do Cells. Não sei se eu usaria em pequenos projetos que só eu mexo, mas a ideia é interessante.
Quanto a gerar HTML dentro do presenter, não vejo como um problema. Ter uma partial com uma única tag já acho que dá mais trabalho.
Como tudo, acho que o que vale é bom senso. Se você está gerando muito HTML no presenter, algo está errado. :)
Acho que utilizando partials pode ficar bem legal, mas concordo que é difícil se acostumar com isto também.
Gostei da ideia do simple_presenter, mas não sei se me acostumaria com isto, a ideia de HTML dentro de código Ruby me parece estranha.
Uma vantagem muito clara é a facilidade dos testes, vou testar em um projeto pequeno e ver como me saio usando esta idéia :D
Só acho que isto dificultaria a interação com designers, caso se tenha a sorte de ter um que mexa em código, como a maioria que conheço entrega imagens com coordenadas, isto não seria um problema :D
Urubatan, você já está acostumado com código Ruby gerando HTML. Ou você não usa form builders e helpers (link_to, por exemplo)?
O que te incomoda é gerar HTML com Ruby sem estar na view. ;)
é a mais pura verdade :D
uso muito helpers e form builders, não tinha pensado nisto, mas nestes casos também não me agrada a mistura, só acho que é a abordagem menos pior :D
e provavelmente os presentes se encaixam na mesma categoria assim que nos acostumamos a utiliza-los.
Vou fazer a experiência e comento alguma coisa depois de brincar um pouco com os presenters :D
Esta gem lembra bastante o Draper que é top #1 do Ruby Toolbox ( https://www.ruby-toolbox.com/categories/rails_presenters )
Eu acho o Presenter um ótimo pattern, infelizmente, pouco usado e divulgado.
Roger, o problema do draper é que ele é muito atrelado ao activerecord.
Gostei,
só uma coisinha que faço differente. Eu sim, vou evitar colocar html la dentro. Não por causa do designer por que no meu caso o meu designer escreve ruby melhor do que eu, mas para poder
usar com outras representações. Ou seja, description por exemplo eu facilmente faria algo assim:
def description
@product.description.present? ? @product.description : "No description available"
end
o que me deixa representar o mesmo presenter com JSON por exemplo tambem.
Jeffry, concordo com você. O problema é que nem sempre dá para seguir essa abordagem. Às vezes (quase sempre?) você simplesmente não quer exibir o elemento sem conteúdo. Neste caso, não tem muita alternativa. É isso ou deixar o if na view! :)
Legal!
Só uma observação, seria necessário mesmo o `config.autoload_paths` no application.rb? Pois acho que estando na pasta `app` já é automagicamente carregado.
Abs
Edison, yep. Já tinha adicionado uma nota no fim do artigo. :)
Ops, pura desatenção, foi mal...
Falando de experiência própria (ter trabalhado um projeto legado de 4 anos em Ruby com um uso de presenters como acima), eu tenho pavor só de ouvir falar. Presenters podem ser interessantes quando usados de forma limitada, mas em uma linguagem e framework como Ruby/Rails, podem ser um belo tiro no pé.
Várias coisas a evitar: composição de presenters, algo bem provável quando o projeto se torna grande; delegações secundárias: se relacionamentos ou composições precisam ser expostas também, o que é algo bem comum, as cadeias de delegações podem se tornar um inferno; mágica demais, principalmente pela tentação de capturar mais detalhes de forma automática no presenter; passar presenters para parciais aninhadas, o que torna a depuração um processo horrendo.
Atualmente, minha abordagem favorita é usar view models, que são bem mais burros mas forçam você a pensar no essencial para aquela view em particular. Infelizmente, o Erb não é lá uma maneira muito bacana para views. A abordagem homoicônica do hiccup (https://github.com/weavejester/hiccup) me atrai muito mas é difícil de reproduzir perfeitamente em Ruby.
Ronaaaaaldo! Concordo com você que pode se tornar um fardo, dependendo das decisões que forem tomadas. Eu ainda não tive nenhum dos problemas que você citou, mas pode ser porque só eu mexo.
Num esquema view-model, gostei *muito* do Mustache. Ainda não testei com o Rails, mas tem sido minha abordagem favorita para Sinatra.
Nando, ao utilizar a sua gem, para tratar a renderização condicionada de partials, me deparei com uma restrição. Por exemplo, se eu "envelopar" um Enumerable (array) no presenter e expôr o método "each", ao tentar utilizá-lo para uma iteração, não consigo iterar pq o método "expose" da classe Presenter não trata adequadamente a passagem de parâmetros para os métodos que sofrem delegação. Neste caso, o bloco da iteração não é repassado ao método "each" do Enumerable. Pra contornar isso, utilizei o módulo Forwardable e deixei somente o método "each" de fora da chamada para "expose".
O que você acha?
Abs
Alexandre, acabei de fazer o release da versão 0.1.2 que tem suporte a iterators. ;)
Nando, sem dúvida. Uma baixo footprint de manutenção, seja trabalhando sozinho ou em uma equipe pequena e disciplinada, segura a onda bastante.
Sobre o Mustache, concordo. Eu não sou grande fã da sintaxe, mas sou muito fã do conceito. Templateless templates. :)
Maravilha, Nando. Versão 0.1.2 funcionando perfeitamente :)
Obrigado. Abraços
E se fizer a aplicação retornar json/xml e utilizar o framework JavascriptMVC?
Veja uma exemplo com rails: http://blog.javascriptmvc.com/?p=68
Assim nos livramos da view server side...
Essa é uma abordagem que nunca pensei em tentar. Quem sabe eu não faça isso em um próximo aplicativo? :)
Deixe um comentário