Post on 12-Sep-2014
description
Model of the Colossus
Mauro quem...
RSpec Best Friends
maurogeorge.com.br
Seu model, um grande colosso
Seu model, um grande colosso
Rails 15 minutes blogMVC
Rails wayAplicações grandes
37 Signals stackERB
MySQLMiniTest
Fat Models, Skinny Controllers
Prime stackHaml
PostgreSQLRspec/Cucumber
Skinny models, controllers, and a service layer
AR quebra o SRP
Alto acoplamentoCallbackObserverFinders
Falta de coesãoPersiste dados
Envia e-mailAcessa Api externas
Anti-pattern
Model gerando conteúdo para a view
class User < ActiveRecord::Base
# ...
def info %Q{ <ul> <li>#{name}</li> <li>#{email}</li> </ul> } endend
app/models/user.rb
Anti-pattern: Model gerando conteudo para a view
Alto acoplamento
Falta de coesão
require 'delegate'
class UserDecorator < SimpleDelegator
def info %Q{ <ul> <li>#{name}</li> <li>#{email}</li> </ul> } endend
app/decorators/user_decorator.rb
Solução: Decorator
Baixo acoplamento
Alta coesão
# Exemplouser = User.find(1)user_decorator = UserDecorator.new(user)user_decorator.infouser_decorator.name
class User < ActiveRecord::Base
# ...
def wrote_post?(post) if post.user_id == id "<p>O post #{post.title} foi escrito por #{name}</p>" end endend
app/models/user.rb
Anti-pattern: Model gerando conteudo para a view
Alto acoplamento
Falta de coesão
Método de Postou User
class WritterPostPresenter
def initialize(user, post) @user, @post = user, post end
def post_is_wrote_by_writter? if wrote_post? "<p>O post #{post.title} foi escrito por #{user.name}</p>" end end
private
attr_reader :user, :post
def wrote_post? user == post.user endend
app/presenters/writter_post_presenter.rb
Solução: Presenter
Baixo acoplamento
Alta coesão
# Exemplouser = User.find(1)post = Post.find(1)writter_post_presenter = WritterPostPresenter.new(user, post)writter_post_presenter.post_is_wrote_by_writter?
Presenters, decorators, exhibit, View Objects e helpers???
HelpersProcedurais
DecoratorsPara uma única entidade
PresentersPara multiplas entidades
Anti-pattern
Model Callbacks
class Post < ActiveRecord::Base
# ...
after_save :notify_users
# ...
private
def notify_users NotifyMailer.delay.notify(self) endend
app/models/post.rb
Anti-pattern: Model Callbacks
Alto acoplamento
Falta de coesão
Testes lentos
class PostCreator
def initialize(post) @post = post end
def save post.save && notify_users end
private
attr_reader :post
def notify_users NotifyMailer.delay.notify(post) endend
app/models/post_creator.rb
Solução: PORO model
Baixo acoplamento
Alta coesão
Testes rápidos
class PostsController < ApplicationController
# ..
def create @post = current_user.posts.new(post_params) if redirect_to posts_path, notice: "Post criado com sucesso!" else render 'new' end endend
app/controllers/posts_controller.rb
Solução: PORO model
@post.savePostCreator.new(@post).save
Anti-pattern
Model salvando N models
class User < ActiveRecord::Base
# ...
accepts_nested_attributes_for :postsend
app/models/user.rb
Anti-pattern: Model salvando N models
Alto acoplamento
Falta de coesão
class UserWithPost include ActiveModel::Model
attr_accessor :user_name, :user_email, :post_title, :post_content validates :user_name, :user_email, :post_title, :post_content, presence: true
def save return false unless valid? user = User.create(name: user_name, email: user_email) user.posts.create(title: post_title, content: post_content) true end
end
app/models/app/models/user_with_post.rb
Solução: Form Object
Baixo acoplamento
Alta coesão
# Exemploparams = { user_name: "Mauro", user_email: "maurogot@gmail.com",
post_title: "Post 1", post_content: "Content"}user_with_post = UserWithPost.new(params)user_with_post.save
Anti-pattern
Scopes para um único problema
class Post < ActiveRecord::Base
# ...
scope :from, ->(user) { where(user_id: user.id) } scope :recents, -> { order(created_at: :asc) } scope :top_likeds, -> { order(like_count: :asc) } scope :top_from, ->(user) { from(user).recents.top_likeds }
# ...
end
app/models/post.rb
Anti-pattern: Scopes para um único problema
Falta de coesão
class TopPostQuery
def initialize(relation = Post.all) @relation = relation.extending(Scopes) end
def top_from(user) @relation.from(user).recents.top_likeds end
module Scopes
def from(user) where(user_id: user.id) end
def recents order(created_at: :asc) end
def top_likeds order(like_count: :asc) end# ...
app/queries/top_post_query.rb
Solução: Query object
Alta coesão
# Exemplouser = User.find(1)top_post_query = TopPostQuery.newtop_post_query.top_from(user)
Anti-pattern
ActiveSupport::Concerns
module Likeable extend ActiveSupport::Concern
def liked_by(user) return false if user_already_liked?(user) up_one_like add_user_as_voted(user) end
def unliked_by(user) return false unless user_already_liked?(user) down_one_like remove_user_as_voted(user) end
private
attr_reader :likeable, :user
def up_one_like # ... end
# ...
def down_one_like # ... end
def add_user_as_voted(user) # ... end
def remove_user_as_voted(user) # ... end
def user_already_liked?(user) # ... endend
app/models/concerns/likeable.rb
Anti-pattern: ActiveSupport::Concerns
Alto acoplamento
Falta de coesão
class Post < ActiveRecord::Base include Likeable
# ...end
app/models/post.rb
Anti-pattern: ActiveSupport::Concerns
Esconde responsabilidade
class LikeManager
def initialize(likeable, user) @likeable, @user = likeable, user end
def like return false if user_already_liked? up_one_like add_user_as_voted end
def unlike return false unless user_already_liked? down_one_like remove_user_as_voted end
private
attr_reader :likeable, :user
def up_one_like # ... end
def down_one_like # ... end
def add_user_as_voted # ... end
def remove_user_as_voted # ... end
def user_already_liked? # ... endend
app/services/like_manager.rb
Solução: Service
Baixo acoplamento
Alta coesão
Única responsabilidade
Duck Typing
# Exemplouser = User.find(1)post = Post.find(1)like_manager = LikeManager.new(post, user)like_manager.likelike_manager.unlike
Bad Smells
Meu Model está virando um Colosso?
class Post < ActiveRecord::Base
# ...
def popular_comments # ... end
def most_viewed_comment # ... end
def most_replied_comment # ... endend
app/models/post.rb
Bad Smell: N métodos com nome de outra entidade
class Post < ActiveRecord::Base
# ...
def self.most_popular_from(user) self.top_posts_from(user) self.more_social_media_repercussion_from(user) # ... end
private
def self.top_posts_from(user) # ... end
def self.more_social_media_repercussion_from(user) # ... endend
app/models/post.rb
Bad Smell: N métodos recebendo o mesmo paramêtro
class Post < ActiveRecord::Base
# ...
def self.most_popular self.most_commented self.more_social_media_repercussion # ... end
private
def self.most_commented # ... end
def self.more_social_media_repercussion # ... endend
app/models/post.rb
Bad Smell: N métodos privados que são usados em apenas um método
Bad Smell
Classe gigante(Provavelmente uma God Class)
Prefira N classes pequenas
Futuro
DCIFuncional
Conclusão
Crie classes
Quebre Model e Classes grandes em classes menores
Divida responsabilidades
Classes que façam apenas uma coisa bem feita
Obrigado
maurogeorge.com.br
Referências
http://rubyweekly.com/archive/124.html
http://rubyweekly.com/archive/126.html
http://robots.thoughtbot.com/post/14825364877/evaluating-alternative-decorator-
implementations-in
http://mikepackdev.com/blog_posts/31-exhibit-vs-presenter
samuelmullen.com/2013/05/the-problem-with-rails-callbacks
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-
models/
http://rubysource.com/ddd-for-rails-developers-part-1-layered-architecture/
http://blog.plataformatec.com.br/2012/03/barebone-models-to-use-with-actionpack-
in-rails-4-0/
http://www.youtube.com/watch?v=DC-pQPq0acs
http://objectsonrails.com/