Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

@cassiomarques http://cassiomarques.wordpress.com

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Quem usa callbacks?

Slide 5

Slide 5 text

Vim aqui para falar que os callbacks do ActiveRecord fedem

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Motivações

Slide 9

Slide 9 text

Muitos projetos com callbacks

Slide 10

Slide 10 text

DOR

Slide 11

Slide 11 text

Não estamos fazendo apenas “sites”

Slide 12

Slide 12 text

Domínios complexos

Slide 13

Slide 13 text

Fat controllers

Slide 14

Slide 14 text

Skinny controllers Fat models

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

Um pouco sobre orientação a objetos

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

COESÃO

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

23 s = Song.new Artist.new("The Cure"), "Pictures of You", 449 24 25 puts s # The Cure - Pictures of You (449)

Slide 22

Slide 22 text

1 class Song 2 attr_accessor :title, :artist, :length 3 4 # ... 5 6 def play 7 # ... código complicado... 8 end 9 end

Slide 23

Slide 23 text

Uma música tocando a si mesma?!

Slide 24

Slide 24 text

1 class AudioDevice 2 def play(song) 3 # Toca a música de alguma forma... 4 end 5 end 6

Slide 25

Slide 25 text

1 device = AudioDevice.new 2 device.play song

Slide 26

Slide 26 text

ActiveRecord

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

Cada objeto representa um registro no banco

Slide 29

Slide 29 text

Cada classe está associada à uma tabela no banco

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

Callbacks do ActiveRecord

Slide 33

Slide 33 text

Métodos invocados em momentos específicos do ciclo de vida de um objeto

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Exemplos

Slide 39

Slide 39 text

Exames de laboratório

Slide 40

Slide 40 text

Uma amostra possui N exames

Slide 41

Slide 41 text

Para cada exame lançado, preciso verificar se ele é o último pendente e então atualizar o status da amostra

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Fácil! Uso um callback!

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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 '

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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) #: 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) #: 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

Slide 49

Slide 49 text

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) #: 0> sample.exams.all? &:finished? 9 => false 10 (pry) #: 0> sample.exams.where("id <> ?", self.id).first.finished? 11 => true 12 (pry) #: 0> self.finished? 13 => true 14 (pry) #: 0> # WTF?

Slide 50

Slide 50 text

O resultado não será persistido enquanto o callback não terminar

Slide 51

Slide 51 text

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) #: 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) #: 0> sample.status 24 => nil

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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 ' 7 8 Finished in 0.07748 seconds 9 1 example, 1 failure

Slide 54

Slide 54 text

AINDA 3? Como assim?

Slide 55

Slide 55 text

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) #: 0> siblings 12 => [] 13 (pry) #: 0> siblings.all? &:finished? 14 => true 15 (pry) #: 0> self.finished? 16 => true 17 (pry) #: 0> # Entra no if... fffuuu 18 => nil

Slide 56

Slide 56 text

Ahhh!!! Estou usando o callback errado!

Slide 57

Slide 57 text

6 after_update :update_sample_if_last_pending_exam

Slide 58

Slide 58 text

1 Finished in 0.06807 seconds 2 1 example, 0 failures

Slide 59

Slide 59 text

Esse tipo de coisa não está documentado (porque não tem como)

Slide 60

Slide 60 text

Para aprender, só fazendo e tomando porrada

Slide 61

Slide 61 text

Detalhes são obscuros

Slide 62

Slide 62 text

Horas debugando...

Slide 63

Slide 63 text

Como fazer para não usar um callback?

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Temos um método com um nome que deixa sua intenção explícita

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Impacto nos testes

Slide 69

Slide 69 text

Ao criar uma amostra, preciso automaticamente criar os exames, dependendo do tipo de amostra

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

Fácil! Uso um callback!

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

1 Finished in 0.06807 seconds 2 1 example, 0 failures

Slide 74

Slide 74 text

(e o tipo da amostra passa a ser uma informação obrigatória)

Slide 75

Slide 75 text

Novo requisito

Slide 76

Slide 76 text

O laboratório tenta realizar os exames sempre dentro do prazo de 2 dias

Slide 77

Slide 77 text

Preciso de um método que indique se a amostra está “atrasada”

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

(OK, eu sei que eu não precisaria necessariamente persistir o objeto aqui, mas é só pra ilustrar :)

Slide 80

Slide 80 text

20 def delayed? 21 created_at < 2.days.ago 22 end

Slide 81

Slide 81 text

1 Finished in 0.07488 seconds 2 3 examples, 0 failures

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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) #: 0> sample.exams.count 14 => 2

Slide 85

Slide 85 text

Novos registros são adicionados ao banco desnecessariamente

Slide 86

Slide 86 text

Isso vai acontecer com qualquer operação de CRUD que você coloque em um callback

Slide 87

Slide 87 text

E seus testes ficarão lentos

Slide 88

Slide 88 text

Como fazer para não usar um callback?

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Para usar, ao invés de...

Slide 92

Slide 92 text

1 class SamplesController < ApplicationController 2 def create 3 @sample = Sample.new params[:sample] 4 end 5 end

Slide 93

Slide 93 text

Faça...

Slide 94

Slide 94 text

1 class SamplesController < ApplicationController 2 def create 3 service = SampleCreationService.new 4 @sample = service.call(params[:sample]) 5 end 6 end

Slide 95

Slide 95 text

LEMBRE-SE:

Slide 96

Slide 96 text

Você NÃO precisa seguir a estrutura de diretórios e tipos de objetos propostos pelo Rails

Slide 97

Slide 97 text

Quem manda é o domínio do problema que sua aplicação resolve

Slide 98

Slide 98 text

Novo requisito

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

Fácil! Uso um callback!

Slide 102

Slide 102 text

No content

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

1 Finished in 0.08499 seconds 2 2 examples, 0 failures

Slide 105

Slide 105 text

Regra está implícita

Slide 106

Slide 106 text

No content

Slide 107

Slide 107 text

Como fazer para não usar um callback?

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Mais claro

Slide 110

Slide 110 text

Regras associadas à exclusão em um único local

Slide 111

Slide 111 text

Se houver diversas regras, considere extrair para um objeto especialista

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

(mas não se esqueça de usar constraints no banco também!)

Slide 115

Slide 115 text

Novo requisito

Slide 116

Slide 116 text

Nosso laboratório terceiriza alguns exames. Precisamos acessar o webservice do fornecedor e criar a solicitação de exame lá...

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

Fácil! Uso um callback!

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

Exame envia a si mesmo (WTF?)

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

Você até poderia mockar o acesso à rede aqui, mas ainda assim a classe estaria fazendo coisa demais

Slide 123

Slide 123 text

Como fazer para não usar um callback?

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

Enviar email usando callbacks

Slide 128

Slide 128 text

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

Slide 129

Slide 129 text

Enviar emails não é responsabilidade do model

Slide 130

Slide 130 text

Mesmo que você estivesse colocando o envio do email em uma fila, estaria errado.

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

Então não posso usar callbacks nunca?

Slide 133

Slide 133 text

pode.

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

Inicializar/normalizar atributos em um callback é OK :)

Slide 136

Slide 136 text

Resumindo

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

Lembre-se que cada objeto deve fazer o mínimo possível de coisas e fazê-las bem

Slide 139

Slide 139 text

Clareza ao invés de código obscuro

Slide 140

Slide 140 text

Convenções são boas para o framework, mas não necessariamente para o seu código

Slide 141

Slide 141 text

Lembre-se que os callbacks serão executados quando você persistir objetos durante a execução dos seus testes

Slide 142

Slide 142 text

O AR::Base serve para criar uma forma padronizada de acessar seu banco de dados

Slide 143

Slide 143 text

Você não precisa colocar todas as regras de negócio lá :)

Slide 144

Slide 144 text

Se você está usando uma linguagem OO e está com medo de criar muitas classes, você está fazendo isso errado

Slide 145

Slide 145 text

Dúvidas?

Slide 146

Slide 146 text

OBRIGADO!