Where Does the Fat Goes? Utilizando Form Objects Para Simplificar seu Código

Post on 19-Jan-2015

95 views 0 download

Tags:

description

Como adicionar novas camadas à sua aplicação MVC para ajudar a manutenção e evolução do código.

Transcript of Where Does the Fat Goes? Utilizando Form Objects Para Simplificar seu Código

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)

• Email

• 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 METZ' RULES FOR DEVELOPERSRubyrogues.ComPoodr.Com

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)

• Email

   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 Implementação do DataMapper Pattern

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 Infraestrutura para form objects

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! guilherme@geteloquent.com

• 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