Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

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.

Avatar for Guilherme Cavalcanti

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