Getting rid of the AR callbacks
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.
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.
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?
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..
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