$30 off During Our Annual Pro Sale. View Details »

Where Does the Fat Goes? Utilizando Form Object...

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

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

Guilherme Cavalcanti

February 22, 2014
Tweet

More Decks by Guilherme Cavalcanti

Other Decks in Programming

Transcript

  1. NÃO VOU FALAR DE REST • Mas o assunto ainda

    são aplicações monolíticas • Outras estratégias para decompor • Form Object
  2. MV "F*" C • Separação de concerns • Baldes •

    Views: apresentação • Controller: Telefonista • Model • Persistência • Domain logic
  3. APLICAÇÃO • Criação de usuário • Criação de loja •

    Envio de emails • Auditoria E-Commerce
  4. 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
  5. 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
  6. 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?
  7. 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
  8. 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
  9. 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
  10. • 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.
  11. 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
  12. 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
  13. 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?
  14. NOVOS BALDES • Novas camadas • Melhor separação de concerns

    • Por muito tempo o Rails não estimulava isso
  15. 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
  16. 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
  17. 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
  18. 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.rb
 class  CaptchaValidator      def  validate_each(r,  attr,  val)          captcha  =  CaptchaQuestion.find(r)   !        unless  captcha.valid?(val)              r.errors.add(attr,  :invalid)          end      end   end  
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. • 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