Design Patterns em Ruby

Guilherme Garnier
@guilhermgarnier
blog.guilhermegarnier.com
rails new projeto
app
├── assets
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── controllers
├── helpers
├── mailers
├── models
└── views
rails new projeto
app
├── assets
│   ├── images
│   ├── javascripts
│   └── stylesheets
├── controllers
├── helpers
├── mailers
├── models
└── views
●

Model
●
●

●

Regras de negócio
Persistência

View
●

●

Exibição de dados

Controller
●

Comunicação entre
model e view
# controller
@categoria = Categoria.find(params[:id])
# view
<img src="<%= @categoria.foto %>" />
...
<h2><%= @categoria.nome %></h2>
<p><%= @categoria.descricao %></p>
# view
<nav class="breadcrumb">
<a href="/receitas">receitas.com</a>
<span> › </span>
<a href="/receitas/massas">massas</a>
<span> › </span>
<span>receitas de lasanha</span>
</nav>
Onde colocar essa lógica?
No model?
●

●

Gerar markup não é responsabilidade do
model
“Single Responsibility Principle” (SRP)
No controller?
●

●

Controllers devem fazer a comunicação
entre models e views
Controllers devem ser “magros”
Na view?
●
●

Código complexo
Difícil de testar
HELPERS
module CategoriaHelper
def breadcrumb(categoria)
...
end
end
# view
<%= breadcrumb(@categoria) %>
module CategoriaHelper
def breadcrumb(categoria)
...
end
def links_subcategorias(categoria)
...
end
def descricao_resumida(categoria)
...
end
end
Será que ninguém passou por
esse problema antes?
DECORATOR
class CategoriaDecorator
attr_reader :categoria
def initialize(categoria)
@categoria = categoria
end
def breadcrumb
...
end
end
# controller
categoria = Categoria.find(params[:id])
@categoria_decorator =
CategoriaDecorator.new(categoria)
# view
<%= @categoria_decorator.breadcrumb %>
<%= @categoria_decorator.categoria.nome %>
module Decorator
attr_reader :model
def initialize(model)
@model = model
end
def method_missing(meth, *args)
if @model.respond_to?(meth)
@model.send(meth, *args)
else
super
end
end
def respond_to?(meth)
@model.respond_to?(meth)
end
end
class CategoriaDecorator
include Decorator
def breadcrumb
...
end
end
# view
<%= @categoria_decorator.breadcrumb %>
<%= @categoria_decorator.nome %>
Problemas com Decorator
Criar um único Decorator para
todas as views?
class CategoriaDecorator
def breadcrumb
…
end
def breadcrumb_admin
…
end
end
Criar um Decorator para
cada view?
class CategoriaDecorator
def breadcrumb
…
end
end
class CategoriaAdminDecorator
def breadcrumb
…
end
end
Criar um Decorator para
cada model?
class DestaquePrincipalDecorator; end
class DestaqueSecundarioDecorator; end
class TopReceitasDecorator; end
class TopChefsDecorator; end
class ReceitasEspeciaisDecorator; end
class CategoriaDestaqueDecorator; end
class HomeController
def show
@destaque_principal_decorator =
DestaquePrincipalDecorator.new(…)
@destaque_secundario_decorator =
DestaqueSecundarioDecorator.new(…)
@top_receitas_decorator =
TopReceitasDecorator.new(…)
@top_chefs_decorator =
TopChefsDecorator.new(…)
@receitas_especiais_decorator =
ReceitasEspeciaisDecorator.new(…)
@categorias_especiais_decorator =
CategoriasEspeciaisDecorator.new(…)
end
end
Criar um único Decorator
para vários models?
Presenter
Exhibit
Presenter
●
●

Um Decorator que decora vários objetos
Também conhecido por outros nomes, como
View Object
class Home
def initialize(destaques, top_receitas, top_chefs, categorias)
end
def top_receitas
# só exibe receitas com foto
end
end
# view
<%= @home.top_receitas.each do |top| %>
<%= render partial: "top_receita", locals: {receita:
top.receita, favoritos: top.favoritos} %>
<% end %>
Exhibit
●

Semelhante ao Presenter

●

Inverte a lógica de visualização

●

O Exhibit é responsável por renderizar a view
class Home
def initialize(..., context)
@context = context
end
def render_top_receitas
@top_receitas.each do |top|
@context.render partial: "top_receita", locals: {receita:
top.receita, favoritos: top.favoritos}
end
end
end
# view
<%= @home.render_top_receitas %>
# controller
@home = Home.new(..., view_context)
# helper
@home = Home.new(..., self)
Onde colocar meus
Design Patterns no projeto?
app
├── assets
├── controllers
├── decorators
├── helpers
├── jobs
├── models
├── presenters
├── services
└── views
Qual é a melhor opção?
●

Não existe bala de prata

●

Analisar a melhor solução para cada caso

●

Não abusar de Design Patterns

●

Questão de gosto pessoal
# app/models/receita.rb
class Receita
include Mongoid::Document
field :nome
field :descricao
... # outros campos da receita
after_save :aplicar_medalhas, :indexar!
after_destroy :aplicar_medalhas, :indexar!
def aplicar_medalhas
Medalhas.apply_to(self.usuario)
end
def indexar!
Sunspot.index!(self)
end
...
# app/models/receita.rb
class Receita
include Mongoid::Document
field :nome
field :descricao
... # outros campos da receita
after_save :aplicar_medalhas, :indexar!
after_destroy :aplicar_medalhas, :indexar!
def aplicar_medalhas
Medalhas.apply_to(self.usuario)
end
def indexar!
Sunspot.index!(self)
end
...
# app/models/receita.rb
class Receita
include Mongoid::Document
field :nome
field :descricao
... # outros campos da receita
after_save :aplicar_medalhas, :indexar!
after_destroy :aplicar_medalhas, :indexar!
def aplicar_medalhas
Medalhas.apply_to(self.usuario)
end
def indexar!
Sunspot.index!(self)
end
...
# app/models/receita.rb
class Receita
def dar_rating(rating)
inc :soma_ratings, rating
inc :total_ratings, 1
end
def rating
return 0 if total_ratings == 0
soma_ratings / total_ratings
end
def serializable_hash
{:id => id.to_s,
:nome => nome,
:descricao => descricao,
:data_envio => data_envio.try(:to_date),
...
}
end
...
# app/models/receita.rb
class Receita
def dar_rating(rating)
inc :soma_ratings, rating
inc :total_ratings, 1
end
def rating
return 0 if total_ratings == 0
soma_ratings / total_ratings
end
def serializable_hash
{:id => id.to_s,
:nome => nome,
:descricao => descricao,
:data_envio => data_envio.try(:to_date),
...
}
end
...
# app/models/receita.rb
class Receita
def dar_rating(rating)
inc :soma_ratings, rating
inc :total_ratings, 1
end
def rating
return 0 if total_ratings == 0
soma_ratings / total_ratings
end
def serializable_hash
{:id => id.to_s,
:nome => nome,
:descricao => descricao,
:data_envio => data_envio.try(:to_date),
...
}
end
...
# app/models/receita.rb
class Receita
def self.busca(opts)
opts[:pagina] = 1 if opts[:pagina].to_i < 1
opts[:por_pagina] = DEFAULT_RESULTS if
opts[:por_pagina].nil?
Sunspot.search(self) do
with(:tipo_prato, opts[:tipo_prato]) if opts[:tipo_prato].present?
without(:foto_url, nil) if opts[:so_com_foto]
with(:quarentena, opts[:quarentena])
with(:is_deleted, false)
paginate :page => opts[:pagina].to_i, :per_page =>
opts[:por_pagina].to_i
end
end
end
# app/models/receita.rb
Sunspot.setup(Receita) do
text :nome, :boost => 10.0
text :descricao
text :tipo_prato
boost {foto.nil? ? 1.0 : 2.5}
boolean(:is_deleted) { destroyed? }
string(:tipo_prato) {tipo_prato.try(:to_slug)}
string(:foto_url) {foto.nil? ? nil : foto.url}
string(:enviada_por) {usuario_id}
end
Responsabilidades da
classe Receita
1. Representar as regras de negócio da receita
2. Mapear os dados da receita no banco
3. Disparar eventos após salvar/excluir receita
(aplicar medalhas e reindexar)
Responsabilidades da
classe Receita
4. Armazenar e calcular avaliações de receitas
feitas pelos usuários
5. Representar uma receita em JSON
6. Executar uma busca de receitas no Solr
7. Configurar o índice de receitas no Solr
Classe Receita refatorada
# app/models/receita.rb
class Receita
include Mongoid::Document
include Rateable
include Searchable
include Receitas::Converters
extend Receitas::Buscas
field :nome
field :descricao
... # outros campos da receita
end
# app/observers/receita_observer.rb
class ReceitaObserver < Mongoid::Observer
def after_save(receita)
Receitas::SolrIndexer.indexar(receita)
Medalhas.aplicar(receita.usuario)
end
def after_destroy(receita)
Receitas::SolrIndexer.indexar(receita)
Medalhas.aplicar(receita.usuario)
end
end
# config/application.rb
module Receitas
class Application < Rails::Application
config.mongoid.observers = :receita_observer
end
end
# app/models/receita/rateable.rb
class Receita
field :soma_ratings, :default => 0
field :total_ratings, :default => 0
module Rateable
def dar_rating(rating)
inc :soma_ratings, rating
inc :total_ratings, 1
end
def rating
return 0 if total_ratings == 0
soma_ratings / total_ratings
end
end
end
# app/models/receitas/converters.rb
module Receitas::Converters
def serializable_hash
{
:id => id.to_s,
:nome => nome,
:descricao => descricao,
:data_envio => data_envio.try(:to_date),
...
}
end
# app/models/receitas/buscas.rb
module Receitas::Buscas
def busca(opts)
opts[:pagina] = 1 if opts[:pagina].to_i < 1
opts[:por_pagina] = DEFAULT_RESULTS if
opts[:por_pagina].nil?
Receita.solr_search do
with(:tipo_prato, opts[:tipo_prato]) if opts[:tipo_prato].present?
without(:foto_url, nil) if opts[:so_com_foto]
with(:quarentena, opts[:quarentena])
with(:is_deleted, false)
paginate :page => opts[:pagina].to_i, :per_page =>
opts[:por_pagina].to_i
end
end
end
# app/models/receita/searchable.rb
class Receita
include Sunspot::Mongoid
module Searchable
included do
searchable do
text :nome, :boost => 10.0
text :descricao
text :tipo_prato
boost {foto.nil? ? 1.0 : 2.5}
boolean(:is_deleted) { destroyed? }
string(:tipo_prato) {tipo_prato.try(:to_slug)}
string(:foto_url) {foto.nil? ? nil : foto.url}
string(:enviada_por) {usuario_id}
end
end
end
end
Como refatorar um “mega model”?
●

Separar responsabilidades
“Single Responsibility Principle”
● Uma classe/módulo para cada responsabilidade
Vantagens
●

●

●
●
●

Mais fácil de testar
Mais fácil de compreender
Mais fácil de manter
Como refatorar um “mega model”?
●

A solução apresentada não é ideal
●
●

●

Usar mixins == herança
A classe Receita continua tendo muitas
responsabilidades e violando o SRP
Baby steps
Referências
Referências
●

●

●

●

●

blog.guilhermegarnier.com/2013/04/design-patterns-emruby-decorators-presenters-e-exhibits/
blog.steveklabnik.com/posts/2011-09-09-better-rubypresenters
robots.thoughtbot.com/post/14825364877/evaluatingalternative-decorator-implementations-in
blog.codeclimate.com/blog/2012/10/17/7-ways-todecompose-fat-activerecord-models/
www.slideshare.net/maurogeorge/model-of-colossus
opensource.globo.com
facebook.com/GloboDev
globodev.tumblr.com
@GloboDev
@guilhermgarnier
blog.guilhermegarnier.com
Slides: goo.gl/6FJU3e

Design Patterns em Ruby

Notas do Editor

  • #7 - http://www.slideshare.net/damiansromek/thin-controllers-fat-models-proper-code-structure-for-mvc - Controllers devem ser “magros”, somente uma fachada para traduzir requests num formato que o model entenda - Models devem conter toda a lógica de negócio