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

Porque você não deve usar os callbacks do ActiveRecord

Porque você não deve usar os callbacks do ActiveRecord

Palestra feita no TDC São Paulo 2012, no dia 07/07/2012

E9202d96b693676af44a254915e1a48f?s=128

cassiomarques

July 07, 2012
Tweet

Transcript

  1. Porque você não deve usar os callbacks do ActiveRecord Cássio

    Marques
  2. @cassiomarques http://cassiomarques.wordpress.com

  3. None
  4. Quem usa callbacks?

  5. Vim aqui para falar que os callbacks do ActiveRecord fedem

  6. None
  7. None
  8. Motivações

  9. Muitos projetos com callbacks

  10. DOR

  11. Não estamos fazendo apenas “sites”

  12. Domínios complexos

  13. Fat controllers

  14. Skinny controllers Fat models

  15. E então começaram a colocar tudo nos models

  16. None
  17. Um pouco sobre orientação a objetos

  18. Objetos devem fazer somente uma coisa (e fazê-la bem)

  19. COESÃO

  20. 1 class Song 2 attr_accessor :title, :artist, :length 3 4

    def initialize(artist, title, length) 5 @artist, @title, @length = artist, title, length 6 end 7 8 def to_s 9 "#{artist} - #{title} (#{length})" 10 end 11 end 12 13 class Artist 14 attr_accessor :name 15 16 def initialize(name) 17 @name = name 18 end 19 20 def to_s; name; end 21 end
  21. 23 s = Song.new Artist.new("The Cure"), "Pictures of You", 449

    24 25 puts s # The Cure - Pictures of You (449)
  22. 1 class Song 2 attr_accessor :title, :artist, :length 3 4

    # ... 5 6 def play 7 # ... código complicado... 8 end 9 end
  23. Uma música tocando a si mesma?!

  24. 1 class AudioDevice 2 def play(song) 3 # Toca a

    música de alguma forma... 4 end 5 end 6
  25. 1 device = AudioDevice.new 2 device.play song

  26. ActiveRecord

  27. None
  28. Cada objeto representa um registro no banco

  29. Cada classe está associada à uma tabela no banco

  30. Implementa uma interface padrão para operações CRUD

  31. AR::Base.create AR::Base#save AR::Base#update_attributes AR::Base.find AR::Base.where etc...

  32. Callbacks do ActiveRecord

  33. Métodos invocados em momentos específicos do ciclo de vida de

    um objeto
  34. 1 class User < ActiveRecord::Base 2 validates :login, :presence =>

    true 3 4 before_validation :ensure_login_has_a_value 5 6 protected 7 def ensure_login_has_a_value 8 if login.nil? 9 self.login = email unless email.blank? 10 end 11 end 12 end
  35. None
  36. None
  37. None
  38. Exemplos

  39. Exames de laboratório

  40. Uma amostra possui N exames

  41. Para cada exame lançado, preciso verificar se ele é o

    último pendente e então atualizar o status da amostra
  42. 1 class Sample < ActiveRecord::Base 2 has_many :exams 3 belongs_to

    :client 4 5 module Status 6 NEW = 1 7 ONGOING = 2 8 FINISHED = 3 9 end 10 end 1 class Exam < ActiveRecord::Base 2 belongs_to :sample 3 4 def finished? 5 !positive.nil? 6 end 7 end
  43. 40 context "when this is the last sample's pending exam"

    do 41 it "updates the sample status to 'finished'" do 42 exam1 = sample.exams.create! :positive => true 43 exam2 = sample.exams.create! :positive => nil 44 expect { 45 exam2.update_attributes :positive => true 46 }.to change { sample.status }.to Sample::Status::FINISHED 47 end 48 end
  44. Fácil! Uso um callback!

  45. 1 class Exam < ActiveRecord::Base 2 belongs_to :sample 3 4

    after_save :update_sample_if_last_pending_exam 5 6 def finished? 7 !positive.nil? 8 end 9 10 private 11 def update_sample_if_last_pending_exam 12 if sample.exams.all? &:finished? 13 sample.update_attributes(:status => Sample::Status::FINISHED) 14 end 15 end 16 end
  46. 1 Failures: 2 3 1) Exam recording results when this

    is the last sample's pending exam updates the sample status to 'finished' 4 Failure/Error: expect { 5 result should have been changed to 3, but is now nil 6 # ./spec/models/exam_spec.rb:44:in `block (4 levels) in <top (required)>'
  47. 13 def update_sample_if_last_pending_exam 14 binding.pry 15 if sample.exams.all? &:finished? 16

    sample.update_attributes(:status => Sample::Status::FINISHED) 17 end 18 end
  48. From: /Users/cassiommc/dev/callbacks/app/models/exam.rb @ line 13 Exam#update_sample_if_last_pending_exam: 13: def update_sample_if_last_pending_exam =>

    14: binding.pry 15: if sample.exams.all? &:finished? 16: sample.update_attributes(:status => Sample::Status::FINISHED) 17: end 18: end (pry) #<Exam>: 0> sample.exams +----+-----------+----------+-------------------------+-------------------------+ | id | sample_id | positive | created_at | updated_at | +----+-----------+----------+-------------------------+-------------------------+ | 35 | 33 | true | 2012-07-02 18:54:55 UTC | 2012-07-02 18:54:55 UTC | | 36 | 33 | | 2012-07-02 18:55:04 UTC | 2012-07-02 18:55:04 UTC | +----+-----------+----------+-------------------------+-------------------------+ 2 rows in set (pry) #<Exam>: 0> self +----+-----------+----------+-------------------------+-------------------------+ | id | sample_id | positive | created_at | updated_at | +----+-----------+----------+-------------------------+-------------------------+ | 36 | 33 | true | 2012-07-02 18:55:04 UTC | 2012-07-02 18:55:59 UTC | +----+-----------+----------+-------------------------+-------------------------+ 1 row in set
  49. 1 13: def update_sample_if_last_pending_exam 2 => 14: binding.pry 3 15:

    if sample.exams.all? &:finished? 4 16: sample.update_attributes(:status => Sample::Status::FINISHED) 5 17: end 6 18: end 7 8 (pry) #<Exam>: 0> sample.exams.all? &:finished? 9 => false 10 (pry) #<Exam>: 0> sample.exams.where("id <> ?", self.id).first.finished? 11 => true 12 (pry) #<Exam>: 0> self.finished? 13 => true 14 (pry) #<Exam>: 0> # WTF?
  50. O resultado não será persistido enquanto o callback não terminar

  51. 1 From: /Users/cassiommc/dev/callbacks/spec/models/exam_spec.rb @ line 45 : 2 3 40:

    context "when this is the last sample's pending exam" do 4 41: it "updates the sample status to 'finished'" do 5 42: exam1 = sample.exams.create! :positive => true 6 43: exam2 = sample.exams.create! :positive => nil 7 44: exam2.update_attributes :positive => true 8 => 45: binding.pry 9 46: # expect { 10 47: # exam2.update_attributes :positive => true 11 48: # }.to change { sample.status }.to Sample::Status::FINISHED 12 49: end 13 50: end 14 15 (pry) #<RSpec::Core::ExampleGroup::Nested_1::Nested_2::Nested_1>: 0> sample.exams 16 +----+-----------+----------+-------------------------+-------------------------+ 17 | id | sample_id | positive | created_at | updated_at | 18 +----+-----------+----------+-------------------------+-------------------------+ 19 | 35 | 33 | true | 2012-07-02 19:06:11 UTC | 2012-07-02 19:06:11 UTC | 20 | 36 | 33 | true | 2012-07-02 19:06:11 UTC | 2012-07-02 19:06:11 UTC | 21 +----+-----------+----------+-------------------------+-------------------------+ 22 2 rows in set 23 (pry) #<RSpec::Core::ExampleGroup::Nested_1::Nested_2::Nested_1>: 0> sample.status 24 => nil
  52. 13 def update_sample_if_last_pending_exam 14 siblings = sample.exams.where "id <> ?",

    self.id 15 if finished? && siblings.all?(&:finished?) 16 sample.update_attributes(:status => Sample::Status::FINISHED) 17 end 18 end
  53. 1 Failures: 2 3 1) Exam recording results when this

    is the last sample's pending exam updates the sample status to 'finished' 4 Failure/Error: expect { 5 result should have changed, but is still 3 6 # ./spec/models/exam_spec.rb:44:in `block (4 levels) in <top (required)>' 7 8 Finished in 0.07748 seconds 9 1 example, 1 failure
  54. AINDA 3? Como assim?

  55. 1 From: /Users/cassiommc/dev/callbacks/app/models/exam.rb @ line 13 Exam#update_sample_if_last_pending_exam: 2 3 13:

    def update_sample_if_last_pending_exam 4 14: siblings = sample.exams.where "id <> ?", self.id 5 => 15: binding.pry 6 16: if finished? && siblings.all?(&:finished?) 7 17: sample.update_attributes(:status => Sample::Status::FINISHED) 8 18: end 9 19: end 10 11 (pry) #<Exam>: 0> siblings 12 => [] 13 (pry) #<Exam>: 0> siblings.all? &:finished? 14 => true 15 (pry) #<Exam>: 0> self.finished? 16 => true 17 (pry) #<Exam>: 0> # Entra no if... fffuuu 18 => nil
  56. Ahhh!!! Estou usando o callback errado!

  57. 6 after_update :update_sample_if_last_pending_exam

  58. 1 Finished in 0.06807 seconds 2 1 example, 0 failures

  59. Esse tipo de coisa não está documentado (porque não tem

    como)
  60. Para aprender, só fazendo e tomando porrada

  61. Detalhes são obscuros

  62. Horas debugando...

  63. Como fazer para não usar um callback?

  64. 40 context "#record_result" do 41 context "when this is the

    last sample's pending exam" do 42 it "updates the sample status to 'finished'" do 43 exam1 = sample.exams.create! :positive => true 44 exam2 = sample.exams.create! :positive => nil 45 expect { 46 exam2.record_result true 47 }.to change { sample.reload.status }.to Sample::Status::FINISHED 48 end 49 end 50 end
  65. 10 def record_result(result) 11 update_attributes :positive => result 12 if

    sample.exams.all?(&:finished?) 13 sample.update_attributes(:status => Sample::Status::FINISHED) 14 end 15 end
  66. Temos um método com um nome que deixa sua intenção

    explícita
  67. Usar os métodos da API do AR::Base não deixa a

    intenção explícita
  68. Impacto nos testes

  69. Ao criar uma amostra, preciso automaticamente criar os exames, dependendo

    do tipo de amostra
  70. 3 describe Sample do 4 context "when created" do 5

    context "with category A" do 6 it "creates the exams for the respective category" do 7 expect { 8 Sample.create! :category => Sample::Category::A 9 }.to change { Exam.count }.by 2 10 end 11 end 12 end 13 end
  71. Fácil! Uso um callback!

  72. 1 class Sample < ActiveRecord::Base 2 after_create :create_exams 3 4

    module Category 5 A = 1 6 B = 2 7 end 8 9 private 10 def create_exams 11 return if category.blank? 12 patologies = case category 13 when Category::A 14 %w(blabla bleble) 15 when Category::B 16 %w(blibli bloblo) 17 end 18 patologies.each do |patology| 19 exams.create :patology => patology 20 end 21 end 22 end
  73. 1 Finished in 0.06807 seconds 2 1 example, 0 failures

  74. (e o tipo da amostra passa a ser uma informação

    obrigatória)
  75. Novo requisito

  76. O laboratório tenta realizar os exames sempre dentro do prazo

    de 2 dias
  77. Preciso de um método que indique se a amostra está

    “atrasada”
  78. 14 describe "#delayed?" do 15 it "returns true if the

    sample was created more than 2 days ago" do 16 sample = Sample.create!( 17 :category => Sample::Category::A, 18 :created_at => 3.days.ago 19 ) 20 sample.should be_delayed 21 end 22 23 it "returns false if the sample was created less than 2 days ago" do 24 sample = Sample.create!( 25 :category => Sample::Category::A, 26 :created_at => 1.day.ago 27 ) 28 sample.should_not be_delayed 29 end 30 end
  79. (OK, eu sei que eu não precisaria necessariamente persistir o

    objeto aqui, mas é só pra ilustrar :)
  80. 20 def delayed? 21 created_at < 2.days.ago 22 end

  81. 1 Finished in 0.07488 seconds 2 3 examples, 0 failures

  82. Sample#delayed? não depende dos exames da amostra

  83. 15 it "returns true if the sample was created more

    than 2 days ago" do 16 sample = Sample.new(:category => Sample::Category::A) 17 sample.created_at = 3.days.ago 18 sample.save! 19 binding.pry 20 sample.should be_delayed 21 end
  84. 1 14: describe "#delayed?" do 2 15: it "returns true

    if the sample was created more than 2 days ago" do 3 16: sample = Sample.new(:category => Sample::Category::A) 4 17: sample.created_at = 3.days.ago 5 18: sample.save! 6 => 19: binding.pry 7 20: sample.should be_delayed 8 21: end 9 22: 10 23: it "returns false if the sample was created less than 2 days ago" do 11 24: sample = Sample.new(:category => Sample::Category::A) 12 13 (pry) #<RSpec::Core::ExampleGroup::Nested_1::Nested_2>: 0> sample.exams.count 14 => 2
  85. Novos registros são adicionados ao banco desnecessariamente

  86. Isso vai acontecer com qualquer operação de CRUD que você

    coloque em um callback
  87. E seus testes ficarão lentos

  88. Como fazer para não usar um callback?

  89. 3 describe SampleCreationService do 4 describe "#call" do 5 let(:sample)

    { stub :exams => exams, :category => category } 6 let(:exams) { stub :create => stub } 7 let(:category) { Sample::Category::A } 8 let(:service) { SampleCreationService.new } 9 10 before { Sample.stub(:create).and_return(sample) } 11 12 it "creates a new sample" do 13 Sample.should_receive(:create).with(:category => category) 14 .and_return(sample) 15 service.call :category => category 16 end 17 18 it "returns the sample" do 19 service.call(:category => category).should == sample 20 end 21 22 it "correctly adds the exams depending on the sample category" do 23 exams.should_receive(:create).with(:patology => "blabla") 24 exams.should_receive(:create).with(:patology => "bleble") 25 service.call :category => category 26 end 27 end 28 end
  90. 1 class SampleCreationService 2 def call(attributes) 3 ActiveRecord::Base.transaction do 4

    Sample.create(attributes).tap do |sample| 5 create_exams_for sample 6 end 7 end 8 end 9 10 private 11 def create_exams_for(sample) 12 return if sample.category.blank? 13 patologies = case sample.category 14 when Sample::Category::A 15 %w(blabla bleble) 16 when Sample::Category::B 17 %w(blibli bloblo) 18 end 19 patologies.each do |patology| 20 sample.exams.create :patology => patology 21 end 22 end 23 end
  91. Para usar, ao invés de...

  92. 1 class SamplesController < ApplicationController 2 def create 3 @sample

    = Sample.new params[:sample] 4 end 5 end
  93. Faça...

  94. 1 class SamplesController < ApplicationController 2 def create 3 service

    = SampleCreationService.new 4 @sample = service.call(params[:sample]) 5 end 6 end
  95. LEMBRE-SE:

  96. Você NÃO precisa seguir a estrutura de diretórios e tipos

    de objetos propostos pelo Rails
  97. Quem manda é o domínio do problema que sua aplicação

    resolve
  98. Novo requisito

  99. Um cliente que possua amostras cadastradas não pode ser excluído

  100. 3 describe Client do 4 context "being deleted" do 5

    it "aborts when the client has samples" do 6 client = Client.create! 7 client.samples.create! :category => Sample::Category::A 8 expect { client.destroy }.to_not change { Client.count } 9 end 10 11 it "succeeds when the client does not have samples" do 12 client = Client.create! 13 expect { client.destroy }.to change { Client.count } 14 end 15 end 16 end
  101. Fácil! Uso um callback!

  102. None
  103. 1 class Client < ActiveRecord::Base 2 attr_accessible :name 3 4

    has_many :samples 5 6 before_destroy :can_be_deleted? 7 8 private 9 def can_be_deleted? 10 samples.empty? 11 end 12 end
  104. 1 Finished in 0.08499 seconds 2 2 examples, 0 failures

  105. Regra está implícita

  106. None
  107. Como fazer para não usar um callback?

  108. 1 class Client < ActiveRecord::Base 2 attr_accessible :name 3 4

    has_many :samples 5 6 def destroy 7 return false unless samples.empty? 8 super 9 end 10 end
  109. Mais claro

  110. Regras associadas à exclusão em um único local

  111. Se houver diversas regras, considere extrair para um objeto especialista

  112. 1 class ClientDeletionChecker 2 def initialize(client) 3 @client = client

    4 end 5 6 def can_delete? 7 !has_samples? && another_rule && yet_another_rule 8 end 9 10 private 11 def has_samples? 12 @client.samples.empty? 13 end 14 15 def another_rule 16 # ... 17 end 18 19 def yet_another_rule 20 # ... 21 end 22 end
  113. 1 class Client < ActiveRecord::Base 2 attr_accessible :name 3 4

    has_many :samples 5 6 def destroy 7 checker = ClientDeletionChecker.new self 8 return false unless checker.can_delete? 9 super 10 end 11 end
  114. (mas não se esqueça de usar constraints no banco também!)

  115. Novo requisito

  116. Nosso laboratório terceiriza alguns exames. Precisamos acessar o webservice do

    fornecedor e criar a solicitação de exame lá...
  117. 44 context "when created" do 45 context "if the exam

    must be sent to a third party" do 46 it "sends the exam" do 47 MyApiWrapper.should_receive(:send_exam) 48 Exam.create! :third_party => true 49 end 50 end 51 52 context "if the exam must not be sent to a third party" do 53 it "does not send the exam" do 54 MyApiWrapper.should_not_receive(:send_exam) 55 Exam.create! :third_party => false 56 end 57 end 58 end
  118. Fácil! Uso um callback!

  119. 1 class Exam < ActiveRecord::Base 2 after_create :send_exam_to_third_party 3 4

    private 5 def send_exam_to_third_party 6 MyApiWrapper.send_exam(self) if third_party? 7 end 8 end
  120. Exame envia a si mesmo (WTF?)

  121. Haverá acesso à rede durante execução dos testes unitários

  122. Você até poderia mockar o acesso à rede aqui, mas

    ainda assim a classe estaria fazendo coisa demais
  123. Como fazer para não usar um callback?

  124. 1 class ExamManager 2 def initialize(sample) 3 @sample = sample

    4 end 5 6 def create(attributes) 7 @sample.exams.create(attributes).tap do |exam| 8 if should_send_to_third_party? exam 9 MyApiWrapper.send_exam(exam) 10 end 11 end 12 end 13 14 private 15 def should_send_to_third_party?(exam) 16 exam.valid? && exam.third_party? 17 end 18 end
  125. 1 class Patology 2 def self.for_category(category) 3 case category 4

    when Sample::Category::A 5 %w(blabla bleble) 6 when Sample::Category::B 7 %w(blibli bloblo) 8 end 9 end 10 end
  126. 1 class SampleCreationService 2 def call(attributes) 3 ActiveRecord::Base.transaction do 4

    Sample.create(attributes).tap do |sample| 5 create_exams_for sample 6 end 7 end 8 end 9 10 private 11 def create_exams_for(sample) 12 return if sample.category.blank? 13 patologies = Patology.for_category sample.category 14 exam_manager = ExamManager.new sample 15 patologies.each do |patology| 16 exam_manager.create :patology => patology 17 end 18 end 19 end
  127. Enviar email usando callbacks

  128. 1 class User < ActiveRecord::Base 2 after_create :send_welcome_email 3 4

    private 5 def send_welcome_email 6 UserMailer.welcome_email(self).deliver 7 end 8 end
  129. Enviar emails não é responsabilidade do model

  130. Mesmo que você estivesse colocando o envio do email em

    uma fila, estaria errado.
  131. 1 class UserCreationService 2 def call(attributes) 3 User.create(attributes).tap do |user|

    4 UserMailer.welcome_email(user).deliver if user.valid? 5 end 6 end 7 end
  132. Então não posso usar callbacks nunca?

  133. pode.

  134. 1 class Sample < ActiveRecord::Base 2 attr_accessible :status, :category 3

    4 has_many :exams 5 belongs_to :client 6 7 before_validation :initialize_attributes 8 9 validates :status, :presence => true 10 11 module Status 12 NEW = 1 13 ONGOING = 2 14 FINISHED = 3 15 end 16 17 private 18 def initialize_attributes 19 self.status = Status::NEW if status.nil? 20 end 21 end
  135. Inicializar/normalizar atributos em um callback é OK :)

  136. Resumindo

  137. Não coloque regras de negócio dentro de callbacks

  138. Lembre-se que cada objeto deve fazer o mínimo possível de

    coisas e fazê-las bem
  139. Clareza ao invés de código obscuro

  140. Convenções são boas para o framework, mas não necessariamente para

    o seu código
  141. Lembre-se que os callbacks serão executados quando você persistir objetos

    durante a execução dos seus testes
  142. O AR::Base serve para criar uma forma padronizada de acessar

    seu banco de dados
  143. Você não precisa colocar todas as regras de negócio lá

    :)
  144. Se você está usando uma linguagem OO e está com

    medo de criar muitas classes, você está fazendo isso errado
  145. Dúvidas?

  146. OBRIGADO!