Date post: | 19-Jan-2015 |
Category: |
Technology |
Upload: | guilherme |
View: | 96 times |
Download: | 0 times |
WHERE DOES THE FAT GOES?UTILIZANDO FORM OBJECTS PARA SIMPLIFICAR SEU CÓDIGO
Guilherme Cavalcanti
github.com/guiocavalcanti
APLICAÇÕES MONOLÍTICAS
• Dependências compartilhadas
• Difícil de modificar
• Difícil de evoluirO Que São?
NÃO VOU FALAR DE REST
• Mas o assunto ainda são aplicações monolíticas
• Outras estratégias para decompor
• Form Object
ROTEIRO • O problema
• Sintomas
• Form objectsSobre O Que Vamos Falar?
O Problema
MV "F*" C
• Separação de concerns
• Baldes
• Views: apresentação
• Controller: Telefonista
• Model
• Persistência
• Domain logic
M V C
Código Inicial
APLICAÇÃO
• Criação de usuário
• Criação de loja
• Envio de emails
• Auditoria
E-Commerce
FAT CONTROLLER
• Inicialização
• Validação (humano)
• Database stuff
• Auditoria (IP)
• Rendering/redirect
def create @user = User.new(user_params) @store = @user.build_store(store_params) ! captcha = CaptchaQuestion.find(captcha_id) unless captcha.valid?(captcha_answer) flash[:error] = 'Captcha answer is invalid' render :new and return end ! ActiveRecord::Base.transaction do @user.save! @store.save! @user.store = @store end ! IpLogger.log(request.remote_ip) SignupEmail.deliver(@user) ! redirect_to accounts_path ! rescue ActiveRecord::RecordInvalid render :new end
SLIM MODEL
• Validação
• Relacionamentos
class User < ActiveRecord::Base has_one :store validates :name, presence: true ! accepts_nested_attributes_for :store end
class Store < ActiveRecord::Base belongs_to :user validates :url, presence: true end
PROBLEMAS
• E se precisássemos de mais de um controller para criar conta?
• Vários pontos de saída
• Acoplamento entre modelos (user e store)
Mas O Que Isso Significa?
CODE SMELLSMartin FowlerRefactoring: Improving The Design Of Existing Code Ruby
CODE SMELLS
• Divergent change
• This smell refers to making unrelated changes in the same location.
• Feature Envy
• a method that seems more interested in a class other than the one it actually is in
def create @user = User.new(user_params) @store = @user.build_store(store_params) ! captcha = CaptchaQuestion.find(captcha_id) unless captcha.valid?(captcha_answer) flash[:error] = 'Captcha answer is invalid' render :new and return end ! ActiveRecord::Base.transaction do @user.save! @store.save! @user.store = @store end ! IpLogger.log(request.remote_ip) SignupEmail.deliver(@user) ! redirect_to accounts_path ! rescue ActiveRecord::RecordInvalid render :new end
SANDI RULES
• Classes can be no longer than one hundred lines of code.
• Methods can be no longer than five lines of code.
• Pass no more than four parameters into a method.
• Controllers can instantiate only one object.
def create @user = User.new(user_params) @store = @user.build_store(store_params) ! captcha = CaptchaQuestion.find(captcha_id) unless captcha.valid?(captcha_answer) flash[:error] = 'Captcha answer is invalid' render :new and return end ! ActiveRecord::Base.transaction do @user.save! @store.save! @user.store = @store end ! IpLogger.log(request.remote_ip) SignupEmail.deliver(@user) ! redirect_to accounts_path ! rescue ActiveRecord::RecordInvalid render :new end
Refactor I
Fat Model, Slim Controller
SLIM CONTROLLER
• Inicialização
• Rendering/redirect def create @user = User.new(params) @user.remote_ip = request.remote_ip @user.save ! respond_with(@user, location: accounts_path) end
• Classes can be no longer than one hundred lines of code.
• Methods can be no longer than five lines of code.
• Pass no more than four parameters into a method.
• Controllers can instantiate only one object.
FAT MODEL
• Criação de Store
• Validação (humano)
• Database stuff
• Auditoria (IP)
class User < ActiveRecord::Base attr_accessor :remote_ip, :captcha_id, :captcha_answer ! has_one :store ! validates :name, presence: true validate :ensure_captcha_answered, on: :create accepts_nested_attributes_for :store ! after_create :deliver_email after_create :log_ip ! protected ! def deliver_email SignupEmail.deliver(@user) end ! def log_ip IpLogger.log(self.remote_ip) end ! def ensure_captcha_answered captcha = CaptchaQuestion.find(self.captcha_id) ! unless captcha.valid?(self.captcha_answer) errors.add(:captcha_answer, :invalid) end end end
CODE SMELLS• Divergent change
• This smell refers to making unrelated changes in the same location.
• Feature Envy
• a method that seems more interested in a class other than the one it actually is in
• Inappropriate Intimacy
• too much intimate knowledge of another class or method's inner workings, inner data, etc.
class User < ActiveRecord::Base attr_accessor :remote_ip, :captcha_id, :captcha_answer ! has_one :store ! validates :name, presence: true validate :ensure_captcha_answered, on: :create accepts_nested_attributes_for :store ! after_create :deliver_email after_create :log_ip ! protected ! def deliver_email SignupEmail.deliver(@user) end ! def log_ip IpLogger.log(self.remote_ip) end ! def ensure_captcha_answered captcha = CaptchaQuestion.find(self.captcha_id) ! unless captcha.valid?(self.captcha_answer) errors.add(:captcha_answer, :invalid) end end end
ACTIVE RECORD
• Precisa do ActiveRecord (specs)
• Acesso a métodos de baixo nível
• update_attributes
• A instância valida a sí mesma
• Difícil de testar
Regras De Negócio No Active Record?
Refactor II
Form Objects
Um Passo A Frente
NOVOS BALDES• Novas camadas
• Melhor separação de concerns
• Por muito tempo o Rails não estimulava isso
FORM OBJECTS
• Delega persistência
• Realiza validações
• Dispara Callbacks
• app/forms
module Form extend ActiveSupport::Concern include ActiveModel::Model include DelegateAccessors ! included do define_model_callbacks :persist end ! def submit return false unless valid? run_callbacks(:persist) { persist! } true end ! def transaction(&block) ActiveRecord::Base.transaction(&block) end end
FORM: O BÁSICO
• Provê accessors
• Delega responsabilidades
• Infra de callbacks
• Realiza validações
• Inclusive customizadas
class AccountForm include Form ! attr_accessor :captcha_id, :captcha_answer ! delegate_accessors :name, :password, :email, to: :user ! delegate_accessors :name, :url, to: :store, prefix: true ! validates :captcha_answer, captcha: true validates :name, :store_url, presence: true end
FORM: ATRIBUTOS
• Alguns são da class
• Alguns são delegados
• delegate_accessors
attr_accessor :captcha_id, :captcha_answer !delegate_accessors :name, :password, :email, to: :user !delegate_accessors :name, :url, to: :store, prefix: true
FORM: VALIDAÇÃO
• Fácil de compor em outros FormObjects
• Não modifica a lógica do Form Object
• Pode ser testada em isolamento
# account_form.rb validates :captcha_answer, captcha: true
!# captcha_validator.rbclass CaptchaValidator def validate_each(r, attr, val) captcha = CaptchaQuestion.find(r) ! unless captcha.valid?(val) r.errors.add(attr, :invalid) end end end
FORM: CALLBACKS
• Dispara callbacks
• Callbacks implementados em classe a parte
• Reutilizáveis
• Pode ser testado em isolamento
# account_form.rb after_persist SendSignupEmail, LogIp !!!class SendSignupEmail class << self def after_persist(form) SignupEmail.deliver(form.user) end end end !class LogIp class << self def after_persist(form) IpLogger.log(form.remote_ip) end end end
FORM: PERSISTÊNCIA
• Delega para os models
• Precisa do ActiveRecord :(
# account_form.rb ! protected ! def store @store ||= Store.new end ! def user @user ||= User.new end ! def persist! transaction do user.save store.save user.store = store end end
SLIM CONTROLLER
• Inicialização
• Rendering/redirect def create @form = AccountForm.new(accout_params) @form.remote_ip = request.remote_ip @form.submit ! respond_with(@form, location: accounts_path) end
SLIM MODEL
• Apenas relacionamentos
• Sem validações
• Sem callbacks
class Store < ActiveRecord::Base belongs_to :user end ! class User < ActiveRecord::Base has_one :store end
CODE SMELL
• Divergent change
• This smell refers to making unrelated changes in the same location.
def persist! transaction do user.save store.save user.store = store end end
PERPETUITY
• Desacopla persistência de lógica de domínio
• Funciona com qualquer PORO
form = AccountForm.new form.name = ‘Guilherme' form.store_url = ‘http://...’ !Perpetuity[Account].insert account
REFORM
• Desacopla persistência de lógica de domínio
• Nesting
• Relacionamentos
• Coerção (usando o Virtus)
@form.save do |data, nested| u = User.create(nested[:user]) s = Store.create(nested[:store]) u.stores = s end
OBRIGADO! [email protected]
• http://pivotallabs.com/form-backing-objects-for-fun-and-profit/
• http://robots.thoughtbot.com/activemodel-form-objects
• http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
• http://www.reddit.com/r/ruby/comments/1qbiwr/any_form_object_fans_out_there_who_might_want_to/
• http://panthersoftware.com/blog/2013/05/13/user-registration-using-form-objects-in-rails/
• http://reinteractive.net/posts/158-form-objects-in-rails
• https://docs.djangoproject.com/en/dev/topics/forms/#form-objects
• http://engineering.nulogy.com/posts/building-rich-domain-models-in-rails-separating-persistence/
• http://robots.thoughtbot.com/sandi-metz-rules-for-developers
• https://github.com/brycesenz/freeform
• http://nicksda.apotomo.de/2013/05/reform-decouple-your-forms-from-your-models/
• http://joncairns.com/2013/04/fat-model-skinny-controller-is-a-load-of-rubbish/
• http://engineering.nulogy.com/posts/building-rich-domain-models-in-rails-separating-persistence/
• https://www.youtube.com/watch?v=jk8FEssfc90