Single Responsibility Principle and Rails

June 10, 2012 📬 Get My Weekly Newsletter

Was reading the slides from Aaron Patterson’s Magma Rails talk and noticed some pretty innocuous Rails code that, upon further reflection is the beginning of disaster for a growing application. As many other Rubyists are beginning to realize, spreading your application logic across only models and controllers leads to a mess. Let’s look at the code, understand why it’s bad, and create a better version.

Here’s the code to create a new user and email them a welcome note:

class UsersController < ApplicationController
  def create
    @user = User.new(params[:user])
    respond_to do |format|
      if @user.save
        UserMailer.deliver_welcome_email(@user)
      end
    end
  end
end

Simple enough, so what’s the problem? Let’s list out the things this class does:

  • creates a new User instance from form parameters
  • saves the new User to the database
  • Sends the user an email if the save was successful
  • Renders the view

This is too many things. Every time we need to add something else that happens when a user is created, we will have to modify this methods. Further, any other part of the system that creates new users will have to duplicate this code (it’s not hard to imagine some sort of user import feature to create new users from some other system).

Fat Models, Skinny Controllers

The Rails Way™ is to put all of this into the model. Using the power of Rails, we could do this:

class UsersController < ApplicationController
  def create
    @user = User.create(params[:user])
  end
end

class User < ActiveRecord::Base

  after_create :deliver_welcome_email

private

  def deliver_welcome_email
    if self.valid?
      UserMailer.deliver_welcome_email(@user)
    end
  end
end

Is this better? Well, our controller is a lot simpler, and now just creates the user and renders the view. That’s pretty much all it should be doing. We’ve deferred our email to an after_create hook.

All we’ve done is move the problem somewhere else. We’ve also made testing our application a huge pain, because everywhere we create a User instance for a test, we’ll fire off the UserMailer, so we’d need to stub that our otherwise arrange for that code not to run, except when we test that code. Ugh.

So, in solving one problem, we’ve created another, giving us two problems, now:

  • The User class is doing too much (even if we count all of ActiveRecord as just “one thing”)
  • We’ve mixed up the concerns of creating instances of User objects with creating new users of our application. The distinction might be subtle, but it’s important.

We can solve both of these problems using the single responsibility principle and by using one of Ruby’s most powerful and, sadly, underused features: creating a new class.

Use Classes

What we want is a single location for “someone new is using our application” and we don’t want that conflated with the creation of the class we use to store that user’s data in the database.

Since our new class is going to create new application users, let’s call it ApplicationUserCreator. I know it’s the Kingdom of Nouns, but the more classes you have, the more specific their names have to be. We could use lambdas, but let’s keep things simple for now.

class ApplicationUserCreator
  # Creates a new user for the application, based on form parameters.
  # Returns the User instance that was created, which might be invalid
  def create_new_user(params)
    User.create(params).tap { |new_user|
      if new_user.valid?
        UserMailer.deliver_welcome_email(new_user)
      end
    }
  end
end

class UsersController < ApplicationController
  def create
    @user = ApplicationUserCreator.new.create_new_user(params[:user])
  end
end

Much better. Our User class remains as it was originally - a class that holds data for the USERS table and provides CRUD operations for it. Our controller is just as skinny in our second example - it launches the new user creation logic and renders the view. We have a new class which is custom built to hold the new application user logic.

These three classes are now very easy to test and very easy to understand; they all simply don’t do that much. Also, the test for our business logic (the test for ApplicationUserCreator) is blazingly fast.

Resilience in the Face of Change

Where a design like this really shines is when we need to add new features to our app.

Suppose we want to do something different when creating administrative users. These users are still stored in the USERS table, but we want to send them a different welcome email (perhaps admin users get a more security-conscious email).

We could start peppering UserMailer with if user.admin? but that’s just wrong, too. The UserMailer already does enough - it emails new application users a welcome email. It does not need to also email administrative users a security-related email. Let’s assume we’ve created AdminUserMailer to handle that. We can also assume we have an AdminUsersController that looks like so:

class AdminUsersController < ApplicationController
  def create
    @user = ApplicationUserCreator.new.create_new_user(params[:user])
  end
end

Identical to UsersController, for now. Our ApplicationUserCreator now needs to check if the new user is an admin. The quick and dirty path, which will get us into trouble, is to check inside that class, like so:

class ApplicationUserCreator
  def create_new_user(params)
    User.create(params).tap { |new_user|
      if new_user.valid?
        if new_user.admin?
          AdminUserMailer.deliver_welcome_email(new_user)
        else
          UserMailer.deliver_welcome_email(new_user)
        end
      end
    }
  end
end

Well, this sucks. We now need to double our tests to handle the case where the new user is an admin. What happens when we add the next few feature? More if statements and more complication. Lets decouple this class from the mailer it uses by allowing the mailer to be injectible:

class ApplicationUserCreator
  def initialize(welcome_mailer=nil)
    @welcome_mailer = welcome_mailer
  end

  def create_new_user(params)
    User.create(params).tap { |new_user|
      if new_user.valid?
        self.welcome_mailer.deliver_welcome_email(new_user)
      end
    }
  end

private

  def welcome_mailer
    @welcome_mailer ||= UserMailer
  end
end

With this design, we can change mailers all we want, and won’t ever need to change ApplicationUserCreator or its tests. We should add a test that UserMailer is the default and that we can inject our own mailer, but at that point, ApplicationUserCreator is a completed class.

AdminUsersController now looks like this:

class AdminUsersController < ApplicationController
  def create
    @user = ApplicationUserCreator.new(AdminUserMailer).create_new_user(params[:user])
  end
end

Not too bad. The classes involved in user creation are all dead simple and easy to test.

Suppose we had a third type of user creation scenario where we don’t want welcome emails to be sent at all? Not a problem:

class NoOpMailer
  def deliver_welcome_email(*args)
  end
end

ApplicationUserCreator.new(NoOpMailer).create_new_user(params[:user])

I realize that Aaron’s code is just an example for a slide at a conference, but I can tell you from experience, that any time an authoritative source shows code to others, they take that as the “right way” to do things. It took the Java community years to stop writing code like this:

try {
  someCode();
}
catch (Exception ex)
  ex.printStackTrace();
}

This code snippet is in every book on Java I’ve ever read, and I get why authors write it, but it’s Just Wrong. So is putting excessive business logic in your controllers or models.

What you can do

It’s very simple. When you are adding code to your Rails app, ask yourself two questions:

  • Is this code about getting data in the right configuration for the view? If not, it does not belong in a controller.
  • Is this code about manipulating data in the database? If not, it does not belong in the model.

Very little of the code you write goes in a controller or model, based on the above criteria. The code goes in some other class, possibly one you will have to create. It doesn’t go in a module that you include into your controller or model. It doesn’t go into a module that you extend your model with at runtime, it goes into a class. That is the unit of code organization in an object-oriented language, so don’t be afraid to use it.