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.

Comentários #

#1 Ronaldo disse:
18 Dez 11, 06:00PM

Presenter novo pattern?? Isso é Strategies!!!

#2 Alberto Souza disse:
18 Dez 11, 06:02PM

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

#3 Rafael Souza disse:
18 Dez 11, 06:44PM

Acho que description não precisa ser mais delegado já que ele foi implementado no presenter, certo?

#4 Nando Vieira disse:
18 Dez 11, 07:30PM

Ronaldo, quem falou que é um novo pattern?

#5 Nando Vieira disse:
18 Dez 11, 07:36PM

Rafael, yep. Você está certo. Vou atualizar os exemplos. :)

#6 Nando Vieira disse:
18 Dez 11, 07:52PM

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.

#7 Leandro N. Camargo disse:
18 Dez 11, 08:01PM

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

#8 Cássio Marques disse:
18 Dez 11, 08:01PM

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?

#9 Juarez Lustosa disse:
18 Dez 11, 08:16PM

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.

#10 Tapajós disse:
18 Dez 11, 08:18PM

Leandro,

Essa sintaxe que o Nando usou é novidade no Ruby. Está certo!

Eu não curti muito mas existe e está certa.

#11 Gabriel Sobrinho disse:
18 Dez 11, 08:46PM

Atualmente utilizo esse approach nos meus projetos: https://gist.github.com/1494843

#12 Nando Vieira disse:
18 Dez 11, 08:55PM

Leandro, tá certa sim. É a sintaxe de hash do Ruby 1.9. :)

#13 Nando Vieira disse:
18 Dez 11, 09:21PM

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. :)

#14 Rodrigo Urubatan disse:
18 Dez 11, 09:36PM

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

#15 Nando Vieira disse:
18 Dez 11, 10:02PM

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. ;)

#16 Rodrigo Urubatan disse:
18 Dez 11, 10:05PM

é 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

#17 Roger Leite disse:
19 Dez 11, 10:03AM

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.

#18 Nando Vieira disse:
19 Dez 11, 10:44AM

Roger, o problema do draper é que ele é muito atrelado ao activerecord.

#19 Jeffry Degrande disse:
19 Dez 11, 11:19AM

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.

#20 Nando Vieira disse:
19 Dez 11, 05:49PM

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! :)

#21 Edison disse:
19 Dez 11, 05:55PM

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

#22 Nando Vieira disse:
19 Dez 11, 07:04PM

Edison, yep. Já tinha adicionado uma nota no fim do artigo. :)

#23 Edison disse:
19 Dez 11, 10:34PM

Ops, pura desatenção, foi mal...

#24 Ronaldo Ferraz disse:
20 Dez 11, 01:54AM

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.

#25 Nando Vieira disse:
20 Dez 11, 02:19PM

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.

#26 Alexandre Cardoso disse:
27 Dez 11, 01:28PM

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

#27 Nando Vieira disse:
28 Dez 11, 12:26AM

Alexandre, acabei de fazer o release da versão 0.1.2 que tem suporte a iterators. ;)

#28 Ronaldo disse:
28 Dez 11, 01:25AM

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. :)

#29 Alexandre Cardoso disse:
28 Dez 11, 09:33AM

Maravilha, Nando. Versão 0.1.2 funcionando perfeitamente :)

Obrigado. Abraços

#30 Bruno disse:
03 Jan 12, 03:18PM

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...

#31 Nando Vieira disse:
03 Jan 12, 04:28PM

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





Não é aceito código HTML: adicione-o no pastie.org ou paste.milk-it.net e poste apenas o link.

Se este é seu primeiro comentário, ele terá que ser aprovado antes de ser exibido.

jQuery: Dominando o framework

Você quer aprender a usar jQuery de verdade? Então chegou a hora! Neste workshop você verá como funciona este framework de JavaScript, entendendo todos os aspectos que fazem do jQuery uma das melhores ferramentas para desenvolvimento de interfaces.

Saiba mais Fechar

Conheça também o HOWTO