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

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