Design Patterns em Ruby

4,523 views

Published on

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

Published in: Technology
0 Comments
6 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,523
On SlideShare
0
From Embeds
0
Number of Embeds
3,397
Actions
Shares
0
Downloads
11
Comments
0
Likes
6
Embeds 0
No embeds

No notes for slide
  • - 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
  • Design Patterns em Ruby

    1. 1. Design Patterns em Ruby Guilherme Garnier
    2. 2. @guilhermgarnier blog.guilhermegarnier.com
    3. 3. rails new projeto app ├── assets │   ├── images │   ├── javascripts │   └── stylesheets ├── controllers ├── helpers ├── mailers ├── models └── views
    4. 4. rails new projeto app ├── assets │   ├── images │   ├── javascripts │   └── stylesheets ├── controllers ├── helpers ├── mailers ├── models └── views
    5. 5. ● Model ● ● ● Regras de negócio Persistência View ● ● Exibição de dados Controller ● Comunicação entre model e view
    6. 6. # controller @categoria = Categoria.find(params[:id]) # view <img src="<%= @categoria.foto %>" /> ... <h2><%= @categoria.nome %></h2> <p><%= @categoria.descricao %></p>
    7. 7. # 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>
    8. 8. Onde colocar essa lógica?
    9. 9. No model? ● ● Gerar markup não é responsabilidade do model “Single Responsibility Principle” (SRP)
    10. 10. No controller? ● ● Controllers devem fazer a comunicação entre models e views Controllers devem ser “magros”
    11. 11. Na view? ● ● Código complexo Difícil de testar
    12. 12. HELPERS
    13. 13. module CategoriaHelper def breadcrumb(categoria) ... end end # view <%= breadcrumb(@categoria) %>
    14. 14. module CategoriaHelper def breadcrumb(categoria) ... end def links_subcategorias(categoria) ... end def descricao_resumida(categoria) ... end end
    15. 15. Será que ninguém passou por esse problema antes?
    16. 16. DECORATOR
    17. 17. class CategoriaDecorator attr_reader :categoria def initialize(categoria) @categoria = categoria end def breadcrumb ... end end
    18. 18. # controller categoria = Categoria.find(params[:id]) @categoria_decorator = CategoriaDecorator.new(categoria) # view <%= @categoria_decorator.breadcrumb %> <%= @categoria_decorator.categoria.nome %>
    19. 19. 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
    20. 20. class CategoriaDecorator include Decorator def breadcrumb ... end end # view <%= @categoria_decorator.breadcrumb %> <%= @categoria_decorator.nome %>
    21. 21. Problemas com Decorator
    22. 22. Criar um único Decorator para todas as views?
    23. 23. class CategoriaDecorator def breadcrumb … end def breadcrumb_admin … end end
    24. 24. Criar um Decorator para cada view?
    25. 25. class CategoriaDecorator def breadcrumb … end end class CategoriaAdminDecorator def breadcrumb … end end
    26. 26. Criar um Decorator para cada model?
    27. 27. class DestaquePrincipalDecorator; end class DestaqueSecundarioDecorator; end class TopReceitasDecorator; end class TopChefsDecorator; end class ReceitasEspeciaisDecorator; end class CategoriaDestaqueDecorator; end
    28. 28. 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
    29. 29. Criar um único Decorator para vários models?
    30. 30. Presenter Exhibit
    31. 31. Presenter ● ● Um Decorator que decora vários objetos Também conhecido por outros nomes, como View Object
    32. 32. 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 %>
    33. 33. Exhibit ● Semelhante ao Presenter ● Inverte a lógica de visualização ● O Exhibit é responsável por renderizar a view
    34. 34. 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 %>
    35. 35. # controller @home = Home.new(..., view_context) # helper @home = Home.new(..., self)
    36. 36. Onde colocar meus Design Patterns no projeto? app ├── assets ├── controllers ├── decorators ├── helpers ├── jobs ├── models ├── presenters ├── services └── views
    37. 37. 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
    38. 38. # 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 ...
    39. 39. # 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 ...
    40. 40. # 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 ...
    41. 41. # 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 ...
    42. 42. # 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 ...
    43. 43. # 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 ...
    44. 44. # 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
    45. 45. # 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
    46. 46. 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)
    47. 47. 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
    48. 48. 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
    49. 49. # 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
    50. 50. # 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
    51. 51. # 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
    52. 52. # 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
    53. 53. # 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
    54. 54. 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
    55. 55. 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
    56. 56. Referências
    57. 57. 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
    58. 58. opensource.globo.com
    59. 59. facebook.com/GloboDev globodev.tumblr.com @GloboDev
    60. 60. @guilhermgarnier blog.guilhermegarnier.com Slides: goo.gl/6FJU3e

    ×