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

Porque você não deve usar os callbacks do Activ...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

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

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

Avatar for cassiomarques

cassiomarques

July 07, 2012
Tweet

More Decks by cassiomarques

Other Decks in Programming

Transcript

  1. DOR

  2. 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
  3. 23 s = Song.new Artist.new("The Cure"), "Pictures of You", 449

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

    # ... 5 6 def play 7 # ... código complicado... 8 end 9 end
  5. 1 class AudioDevice 2 def play(song) 3 # Toca a

    música de alguma forma... 4 end 5 end 6
  6. 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
  7. Para cada exame lançado, preciso verificar se ele é o

    último pendente e então atualizar o status da amostra
  8. 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
  9. 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
  10. 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
  11. 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)>'
  12. 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
  13. 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
  14. 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?
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. (OK, eu sei que eu não precisaria necessariamente persistir o

    objeto aqui, mas é só pra ilustrar :)
  25. 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
  26. 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
  27. 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
  28. 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
  29. 1 class SamplesController < ApplicationController 2 def create 3 service

    = SampleCreationService.new 4 @sample = service.call(params[:sample]) 5 end 6 end
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. Nosso laboratório terceiriza alguns exames. Precisamos acessar o webservice do

    fornecedor e criar a solicitação de exame lá...
  36. 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
  37. 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
  38. Você até poderia mockar o acesso à rede aqui, mas

    ainda assim a classe estaria fazendo coisa demais
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. Se você está usando uma linguagem OO e está com

    medo de criar muitas classes, você está fazendo isso errado