Using Object-Oriented Programming to Manage Control Flow in Rails Controllers

This post made the rounds the other day and really left us all scratching our heads. In the post, the author gives an example of how he relies on the use of exceptions as a sort of modern day GOTO statement instead of using conditionals to manage control flow in a controller. His assertion that his typical approach to writing controller code involves “… put[ting] a bunch of conditionals in there for different situations” hints at a deeper problem that I’ve seen in a lot of Ruby and Rails applications.

Instead of working to refactor the code in the controller, I’d like to back up a bit and take a look at an alternative refactoring that relies on OO concepts while taking advantage of some of the facilities that Rails has to offer. In the end, we’ll be left with more idiomatic Ruby and Rails code that is also easier to understand and maintain.

The Problem

The particular portion of the application that the author discusses handles exchanging Twitter credentials for the application’s credentials. In the controller flow, he does a few things:

  1. Ensures valid parameters are passed
  2. Finds a matching user for the supplied credentials
  3. Creates a “device” which is responsible for issuing an API key
  4. Respond to the client in a meaningful way

At steps 1 and 2, there is the potential for an error condition that needs to be handled (e.g. missing parameters, non-existent user).

Modeling Resources

Since the release of Rails 2, we’ve been taught to use REST and think of concepts in the application’s domain in terms of resources and model them as such. In many cases, those resources map directly to entities in the application’s data store, but they can also represent conceptual entities and have nothing to do with persistence.

In thinking about naming the conceptual entity present in this code example, I could call it a TwitterCredential or simply just a Credential since it’s possible that this is the only way to sign-in with the service (I’m assuming this is an example from the ShortMail application). Assuming that name makes sense in the application domain, I now have the start of my resource:

class Credential
end

Validating Parameters

As a result of the refactorings that were part of the migration from Rails 2 to 3, Rails developers now have useful modules we can apply to our own classes that aren’t ActiveRecord classes. One such refactoring was the extraction of validations into the ActiveModel::Validations module that I will use to address the first responsibility of the controller:

class Credential
  include ActiveModel::Validations

  attr_accessor :screen_name, :oauth_token, :oauth_secret

  validates_presence_of :screen_name, :oauth_token, :oauth_secret, :message => 'required'
end

Adding this gives me the valid? method on the object and ensures that the 3 required parameters are present. This subtly changes the API response from the original code, but I think it’s an improvement – I’ll discuss that change later.

Finding a User

Once I’m finished with validation, I need to have a user object available to me to both further validate the supplied credentials and create a device that I will use in the response from the controller. One way to implement this would be to lazily load the user to suit both purposes:

class Credential
  include ActiveModel::Validations

  validate :user_exists, :unless => :errors?

  private

  def errors?
    errors.any?
  end

  def user
    @user ||= User.by_screen_name(screen_name).where({
      :oauth_token  => oauth_token,
      :oauth_secret => oauth_secret
    }).first
  end

  def user_exists
    errors.add(:user, 'not found') unless user.present?
  end
end

Validation will fail if the user does not exist and, if it does, will be loaded into an instance variable to be used later.

Create a Device

Now that I have a user, I can combine it with additional attributes (token and description from the controller parameters). I’ll add those to our attributes and create the device if all goes well:

class Credential
  include ActiveModel::Validations

  attr_accessor :screen_name, :oauth_token, :oauth_secret, :token, :description

  def save
    valid? && create_device
  end

  private

  def create_device
    @device = Device.find_or_create_by_token!({
      :token       => token,
      :description => description,
      :user_id     => user.id
    })
    !@device.nil?
  end
end

I store the created device in an instance variable to handle the last part:

Responding in a Meaningful Way

I can now make the Credential resource responsible for its own response:

class Credential
  def as_json(options = {})
    {:api_key => @device.api_key}
  end
end

This handles the case where the controller responds successfully – I will continue to leave the error response as a responsibility of the controller.

Tying it Together

In order to have this code fit neatly into a controller action, I need to tweak the API for my resource a bit:

class Credential
  def initialize(attributes = {})
    attributes.each {|k, v| set_recognized_attribute(k, v) }
  end

  private

  def set_recognized_attribute(name, value)
    setter_method = "#{name}="
    self.send(setter_method, value) if respond_to?(setter_method)
  end
end

This allows me to just pass parameters that I care about from the controller to my resource since Rails likes to add additional key/value pairs to this collection (e.g. action and controller). Reworking the controller is now a simple matter and resembles what should just be boilerplate controller code:

class CredentialsController < ApplicationController
  def create
    credential = Credential.new(params)
    if credential.save
      render :json => credential
    else
      render :json => {:error => errors_for(credential)}, :status => :unprocessable_entity
    end
  end

  private

  def errors_for(object)
    object.errors.map {|k, m| "#{k} #{m}" }
  end
end

You’ll notice the API change that I mentioned earlier – my Credential resource now has a collection of errors due to how ActiveModel validations work. While this is a departure from the original example, I think that this is a subtle improvement that makes the API more user-friendly during debugging.

Conclusion

When solving a particular problem in an OO language, it often makes sense to take a step back and think about the conceptual entities involved and how they collaborate with one another. By adhering to some basic design principles, notably the separation of concerns and encapsulation, we have a solution that is easier to understand and maintain.

The resulting code from this refactoring is available as a gist.

Patrick is development director in Viget's Boulder, CO, office. He writes clean Ruby code and automates system infrastructure for clients such as Shure and Volunteers of America.

More posts by Patrick