Getting rid of the AR callbacks
Problem
When writing a small rails application, it is really easy to use ActiveRecord callbacks. You want to send a welcome message after user signs up? Add after_create callback and voila.
Your model might look something like this:
class User < ActiveRecord::Base
validates :name, :presence => true
after_create :send_welcome_email
private
def send_welcome_email
UserNotifier.welcome(self).deliver
end
end
It seems nice so far. Lets also add some tests to ensure it works:
require "spec_helper"
describe User do
it "should send a welcome email after the user is created" do
user = User.new(:name => "John")
notifier = stub
notifier.should_receive(:deliver)
UserNotifier.stub(:welcome).with(user) { notifier }
user.save
end
end
This works, but there are some issues:
- It’s slow. Saving records to the database is something we don’t want to do.
- We have to stub out welcome message part in every test that creates an user.
- We also know that the site administrator later wants to add users manually using admin panel where welcome messages shouldn’t be sent.
There are some solutions for these issues:
- Instead of saving the user we can use user.run_callbacks(:create) { false }.
- We can build a helper method that stubs out sending welcome message.
- We can add an if condition to the callback that checks some attr_accessor variable.
There’s something common with each of these points: they suck! The reason why they suck is because they shouldn’t be in the model at all.
Solution
Instead of adding after_create callback just create a separate class that handles this logic. Lets create a new directory called services and add a new class:
class UserRegistrationService
def initialize(name)
@user = User.new(:name => name)
end
def register
if @user.save
send_welcome_email
end
@user
end
private
def send_welcome_email
UserNotifier.welcome(@user).deliver
end
end
Testing this class is much easier. We don’t even have to load rails.
require_relative "../../app/services/user_registration_service"
class User; end
class UserNotifier; end
describe UserRegistrationService do
let(:user) { stub(:save => true) }
let(:notifier) { stub(:deliver => true) }
before do
User.stub(:new).with(:name => "John") { user }
UserNotifier.stub(:welcome).with(user) { notifier }
end
it "returns the user" do
UserRegistrationService.new("John").register.should == user
end
it "sends a welcome message after the user is created" do
notifier.should_receive(:deliver)
UserRegistrationService.new("John").register
end
it "does not send a welcome message when saving the user fails" do
user.stub(:save) { false }
notifier.should_not_receive(:deliver)
UserRegistrationService.new("John").register
end
end
There are many testing benefits for this approach:
- We don’t have to load rails. (no spec_helper loaded). This means the testing time is really short.
- There is no need to save anything to the database. Again, fast tests.
- We don’t have to worry about stubbing out the welcome message part in the other tests. We only have to worry about it in one place.
- We can use the same class in the integration testing if we need to populate the database.
Other benefits:
- The User model is clean. It is easier to maintain shorter classes.
- If we don’t want to send emails, we can just use some other class or the AR model directly.
When is it okay to use callbacks?
In my opinion - never.
I really like José Valim tweet: If you want to skip an Active Record callback, it probably shouldn’t be a callback.. He later added: Scratch that. Most of your Active Record callbacks probably shouldn’t be a callback..
Where to now?
I suggest reading a free book called Object on Rails.
If you already haven’t, then take a look at Destroy All Software screencasts.
There are also a few older blog posts that I found interesting regarding this topic: ActiveRecord’s Callbacks Ruined My Life and Crazy, Heretical, and Awesome: The Way I Write Rails Apps