Um projeto Rails segue o modelo MVC padrão, que funciona bem para muitos projetos simples. Porém, conforme seu projeto cresce e fica mais complexo, essa arquitetura se mostra muito limitada. Nesta palestra, apresentei problemas reais onde essa arquitetura não é suficiente. Apresentei alguns design patterns úteis para deixar sua arquitetura mais flexível e mais fácil de testar.
Palestra apresentada nos eventos FISL 14 (04/07/2013) e RS on Rails (19/10/2013).
Mais informações: http://blog.guilhermegarnier.com/2013/07/minha-palestra-no-fisl-14-design-patterns-em-ruby/
Vídeo da palestra: https://vimeo.com/69973911
25. 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
41. class DestaquePrincipalDecorator; end
class DestaqueSecundarioDecorator; end
class TopReceitasDecorator; end
class TopChefsDecorator; end
class ReceitasEspeciaisDecorator; end
class CategoriaDestaqueDecorator; end
42. 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
46. 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 %>
48. 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 %>
50. Onde colocar meus
Design Patterns no projeto?
app
├── assets
├── controllers
├── decorators
├── helpers
├── jobs
├── models
├── presenters
├── services
└── views
51. 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
52. # 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
...
53. # 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
...
54. # 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
...
55. # 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
...
56. # 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
...
57. # 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
...
58. # 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
59. # 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
60. 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)
61. 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
62. 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
63. # 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
64. # 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
66. # 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
67. # 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
68. 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
69. 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
- 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