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

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

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

2681801387eec3f6e825d721452a3254?s=128

Guilherme Cavalcanti

February 22, 2014
Tweet

More Decks by Guilherme Cavalcanti

Other Decks in Programming

Transcript

  1. WHERE DOES THE FAT GOES? UTILIZANDO FORM OBJECTS PARA SIMPLIFICAR

    SEU CÓDIGO
  2. Guilherme Cavalcanti github.com/guiocavalcanti

  3. None
  4. APLICAÇÕES MONOLÍTICAS • Dependências compartilhadas • Difícil de modificar •

    Difícil de evoluir O Que São?
  5. NÃO VOU FALAR DE REST • Mas o assunto ainda

    são aplicações monolíticas • Outras estratégias para decompor • Form Object
  6. ROTEIRO • O problema • Sintomas • Form objects Sobre

    O Que Vamos Falar?
  7. O Problema

  8. MV "F*" C • Separação de concerns • Baldes •

    Views: apresentação • Controller: Telefonista • Model • Persistência • Domain logic
  9. M V C

  10. Código Inicial

  11. APLICAÇÃO • Criação de usuário • Criação de loja •

    Envio de emails • Auditoria E-Commerce
  12. 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
  13. 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
  14. 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?
  15. CODE SMELLS Martin Fowler
 Refactoring: Improving The Design Of Existing

    Code Ruby
  16. 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
  17. SANDI METZ' RULES FOR DEVELOPERS Rubyrogues.Com
 Poodr.Com

  18. 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
  19. Refactor I

  20. Fat Model, Slim Controller

  21. 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
  22. • 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.
  23. 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
  24. 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
  25. 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?
  26. Refactor II

  27. Form Objects Um Passo A Frente

  28. NOVOS BALDES • Novas camadas • Melhor separação de concerns

    • Por muito tempo o Rails não estimulava isso
  29. 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
  30. 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
  31. 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
  32. 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  
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Perpetuity Implementação do DataMapper Pattern

  39. 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
  40. Reform Infraestrutura para form objects

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

  43. • 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