Skinny Models – the DAO pattern for Ruby persistence

20 Jul 2013

For those used to ActiveRecord-style development the following might not seem all that bad:

class User < ActiveRecord::Base
  belongs_to :account
  has_many   :posts

  validates_format_of :email, :with => /@/

  after_create :deliver_welcome_email
  after_create :mark_pending

  private

  def mark_pending
    update_attributes! :state => 'pending'
  end

  def deliver_welcome_email
    ApplicationMailer.deliver :welcome_email, email
  end
end

If you’ve read Sandi Metz’ book (or have an OO background) you’ll notice something about this conventional Rails code. Not only is it a huge mess of dependencies, it violates damn near every part of [SOLID](http://en.wikipedia.org/wiki/SOLID_(object-oriented_design) object-oriented design that we practice as a means of keeping our code nimble.

So what’s the alternative?. The Data Access Object pattern uses an object to manage persistence and doesn’t let that object do anything else. Effectively, it makes the above class look like this:

class User < ActiveRecord::Base
  belongs_to :account
  has_many :posts
end

Or, if you want to get religious:

class User < ActiveRecord::Base
end

(Personally, I think associations are the thing ActiveRecord does right so I’m fine leaving those in.)

We just took a lot of code out of the User class and it has to go somewhere else. Where? Well, that depends on what it does. Consider this reorganization:

class User < ActiveRecord::Base
  belongs_to :account
  has_many :posts
end

module Validator
  class Invalid < StandardError; end
                                   
  def self.validate!(record)
    unless record.email =~ /@/   
      raise Invalid
    end
  end
end

module Email
  def self.welcome(email)
    ## Handle email delivery
  end
end

class StateMachine

  attr_reader :record
  def initialize(record)  # I'm making the StateMachine instantiable because
    @record = record      # the 'record' object feels like internal state and
  end                     # there may be actions we want to perform on that
                          # object during a state transition
  def pending!
    record.state = 'pending'
    record.save!
  end
end

module Signup
                                   # You now have to use this to tell Signup
  def self.persistence=(db_model)  # what object is in charge of database
    @persistence = db_model        # persistence. This is where you pass in
  end                              # your DAO or, in tests, your stub object
                                 
  def self.complete! data              # I'm not bothering to make Signup an instantiable
    record = @persistence.new(data)    # class at this point because we don't need it yet.
    Validator.validate! record
    record.save!
    StateMachine.new(record).pending!
    Email.welcome data[:email]
  end
end

We now have many more lines of code and many more objects so it might look like we just added complexity but in fact we’ve reduced it. If complexity is the entanglement of concepts, then we’ve removed nearly all complexity from this application. Let’s walk through the changes bit by bit.

Removing callbacks

We turned a couple after_create callbacks into direct code execution. You now know exactly when the email address will be validated and when the welcome message will be sent. You also know how to stub out the objects that perform those functions in your tests.

Introducing a proper state machine

This is my favorite. There’s a Rails application at work that moves a lot of money around and needs to handle many intermediate states with very precise rules about when things are purchased, paid for, approved, rejected, returned, refunded, etc. If we were to try to implement this logic inside an ActiveRecord class we’d be fighting against the weight of Rails every time we extended the state graph.

Most Rails applications I’ve worked on have eventually had some kind of state machine in them. I’ve tried acts_as_state_machine, two gems both called state_machine and I’ve built several terrible and bug-prone versions myself. Nothing has worked as well as pulling all the logic out into a new class and being very explicit about what kind of state transitions your application allows. You’ll notice the interface to the persistence object is only two methods large. As long as the object you pass in responds to state= and save! the StateMachine class will work. Which means you do NOT need to pass in an ActiveRecord instance in your tests – any simple mock will do.

Adding sane validations

Two months ago I was in the middle of a Rails2->Rails3 upgrade and I found myself in a pry console staring at the insides of a validates_each block deep inside Rails. For trivial validations and/or trivial applications the Rails way of adding data validation is just fine. But when you write the validations yourself you get all the following benefits:

  • test your validations without simultaneously testing that ActiveRecord#save still works
  • test the edge cases of your validations because you actually control how they’re defined and when they fire
  • easily debug your validations because you implemented them in a well-factored class
  • write hundreds of validation tests if you want because they’ll all run in less than a second.

Separating application-level features from database objects

The conventional path would have you type User.create! params[:user] in your controller. This is only good if you’re certain that creating a new User record is the action you’re trying to complete. But more likely what you want is to complete a user signup. So Signup.complete! params[:user] is much more descriptive of what you want. Maybe you want to create a User record. Maybe you want to create a Business record. Maybe it’s such an important action that you’ll be generating Account, Business, User, and Product records all at once. By keeping the controller-level semantics high-level you won’t have to change them to keep up with an evolving implementation.

And then your tests go zoom

This post isn’t called ‘how to make your Rails tests faster’ but it might as well be. The reason your tests are slow is ActiveRecord and ActionController. Your code (and Ruby) is actually blazing fast but it’s organized in such a way that you have to test each object’s dependencies if you want to test the object itself.

The tests

Let’s take a look at what these tests would look like before and after the above reorganization. First, the conventional Rails way:

## file:spec/models/user_spec.rb
require 'config/environment'  # This loads all of Rails and autoloads
describe User do              # every object automatically
  describe 'creating' do
    subject { User.create! params }
    let(:email) { 'm@rvelo.us' }
    let(:name)  { 'Marvelous'  }
    let(:params) { {
      email: email,
      name:  name
    } }
    context "with invalid email" do
      let(:email) { 'whoops' }
      it "doesn't create a new record" do
        expect { subject }.to_not change { User.count }
      end
    end
    context "with valid params" do
      it "creates a new record" do
        expect { subject }.to change { User.count }.by(1)
      end
      it "sends a welcome email" do
        ActionMailer.should_receive(:deliver).with(:welcome, email)
        subject
      end
      it "sets the 'state' column to 'pending'" do
        subject
        User.last.state.should == 'pending'
      end
    end
  end
end

That’s a respectable spec. It’s reasonably clean, tests most of the important stuff, and doesn’t go off track testing unnecessary edge cases or the implementation of the code (other than the ActionMailer bit).

However, every example in that snippet booted all of ActiveRecord and tested the full forest of callbacks, validations, database connection mechanisms, and relational associations inside ActiveRecord. Rails has its own tests so there’s no point in me or you re-testing the Rails internals. That just turns our app into a Seti@Home that’s guaranteed not to find anything.

Let’s rewrite that to test only what we care about.

## file:spec/unit/services/signup_spec.rb
require 'app/services/signup'  # We only require the thing we care about
                               # and we trust that _it_ will require any
                               # require any dependencies that it needs.
module FakeDAO
  require 'ostruct'               # We don't want to go saving to the actual
  def self.instance               # database all the time, we'll just assume that
    @instance ||= OpenStruct.new  # ActiveRecord still works.
  end

  def self.new(attrs)
    attrs.each do |k,v|
      instance.send "#{k}=", v
    end
    instance.send "save!=", true
    instance
  end
end

describe Signup do

  before { Signup.persistence = FakeDAO } # This is where we inject the dependency
  subject { Signup.complete! data }

  let(:email) { 'm@rvelo.us' }
  let(:name)  { 'Marvelous'  }
  let(:data) { {
    email: email,
    name:  name
  } }

  context "with invalid email" do
    let(:email) { 'whoops' }
    it "doesn't create a new record" do
      expect { subject }.to raise_error(Validator::Invalid)
    end
  end
  context "with valid data" do
    it "creates a new record" do
      FakeDAO.instance.should_receive(:save!).at_least(1).times
      subject
    end
    it "sends a welcome email" do
      Email.should_receive(:welcome).with(email)
      subject
    end
    it "sets the 'state' column to 'pending'" do
      subject
      FakeDAO.instance.state.should == 'pending'
    end
  end
end

That’s about the same size code as the previous example but it runs in less than a hundredth of a millisecond. It also explicitly tests only Signup – the object under test.

We then get to add individual tests for the other components we’ve extracted. Each of those will be focused and obvious.

You’re stubbing things. What if a bug slips through the cracks?

It’s only okay to stub the dependencies of an object if two conditions hold: You’re stubbing in such a way that you’re unlikely to change the core behavior of the object under test and you still have high-level integration tests that test the whole suite.

So if you’re reasonably sane about what you choose to stub and you still write a basic high-level test that only tests for the primary cases (not edge cases) you can get away with this approach.

We’ve had enough success with this pattern at work that Xavier added the following to our spec/unit_helper.rb:

RSpec.configure do |config|
  config.before(:suite) do
    if Object.const_defined?("Rails") || Object.const_defined?("ActiveRecord")
      raise "The Rails environment should not be loaded for unit tests."
    end
  end
end

That’s right, we throw an error if you’ve required any part of Rails in your code. This enforces that you’ve written loosely-coupled, dependency-injected, DAO-style persistence-oriented classes whether you are familiar with those terms or not. I can’t recommend this snippet of code highly enough.


Please if you found this post helpful or have questions.