Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Form-backing objects for fun and profit

In this post I’ll make the case for why form-backing objects help keep your app’s codebase clean and maintanable, and
I’ll show how to create, use and test a form-backing object.

Imagine that you are building a website where users can register and your stories look something like this:

  • When a visitor registers a User record and Business record are created
  • The user must accept the terms of service
  • The user must correctly answer a simple math or logic question to ward off bots
  • The site should log the IP address the of the user who registered
  • After the registration is complete, the user should get an email
  • If there are any problems or validation errors during registration, no User or Business should be created and nothing should be logged

These are pretty straightforward requirements, so you start coding:

Take 1: The Fat Controller

The first version might look something like this:

class UsersController < ApplicationController
  def new
    @question = AntiBotQuestion.random
    @user = User.new
  end

  def create
    @business = Business.new(params[:business])
    @user = User.new(params[:user])
    @question = AntiBotQuestion.find_by_id(params[:anti_bot_question_id])

    objects_were_saved = false
    begin
      ActiveRecord::Base.transaction do
        @business.save!
        @user.save!
        @business.memberships.create! :user => @user
      end
      objects_were_saved = true
    rescue
    end

    if objects_were_saved && @question.answer == params[:anti_bot_question][:answer]
      UserMailer.deliver_user_registered(@user)
      IpLogger.log(request.remote_ip)
      redirect_to root_path
    else
      render :action => "new"
    end
  end
end

If you’re like most Rails developers I know, you probably vomited a little in your mouth after looking at that code.
At first glance its biggest issue is that is violates the skinny-controller-fat-model guideline that’s become popular
recently, so following that guideline you decide to put your controller on a diet:

Take 2: The Fat Model

In an effort to make the controller skinny, you move most of the registration logic to the User model like so:

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

    if @user.save
      redirect_to root_path
    else
      render :action => "new"
    end
  end
end


class User < ActiveRecord::Base
  belongs_to :membership
  accepts_nested_attributes_for :membership

  validates_confirmation_of :terms_of_service

  validate :on => :create do
    errors[:base] << "Incorrect!" if answer != question.answer
  end

  attr_accessor :ip_address, :question, :answer

  after_create do
    UserMailer.deliver_user_registered
    IpLogger.log(ip_address)
  end
end

Skinny controller, fat model, ship it – right? Well, if I had to make the choice, I would choose the fat-controller option any day.
While the fat-controller code is ugly, at least it’s isolated. The fat model code:

  • pollutes the entire model – tightly coupling User to Business
  • adding network calls in models
  • adding controller concerns like IP addresses to the model

Practically speaking, it adds overhead to every User creation throughout the app. Let’s say for example that you merge
codebases with another site (like through an acquisition) and you need to add all the other site’s users to your user
base – do you add fake IP addresses and confirmations, or do you add conditional logic to skip those callbacks? In addition,
you have overhead when creating a User in your test suite.

After looking at that, you might decide to introduce a form-backing object.

Take 3: Skinny Controller, Skinny Model, and Form-backing Object

Form-backing objects, also known as Presenters (not to be confused with the concept of view presenters), are objects
whose sole purpose is to take user-entered form data and perform some unit of work. Creating and testing form-backing objects
is simple. In this situation, you might add a Registration object.

The controller remains very simple:

class RegistrationsController < ApplicationController
  def new
    @registration = Forms::Registration.new
  end

  def create
    @registration = Forms::Registration.new(params[:registration].merge(:ip_address => request.remote_ip))
    if @registration.save
      redirect_to root_path
    else
      render :action => "new"
    end
  end
end

The form becomes much simpler than either of the cases above, since there are no nested forms or multiple instance variables:

<%= form_for @registration, :url => registrations_path, :as => :registration do |f| %>
  <%= f.error_messages %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  <%= f.label :email %>
  <%= f.text_field :email %>
  <%= f.label :anti_bot_answer, f.object.anti_bot_question.text %>
  <%= f.text_field :anti_bot_answer %>
  <%= f.check_box :terms_of_service %>
  <%= f.label :terms_of_service %>
  <%= f.hidden_field :anti_bot_question_id, :value => f.object.anti_bot_question.id %>
  <%= f.submit %>
<% end %>

The form-backing object must conform to the ActiveModel interface, in addition to whatever interface you defined in your controller.
This is an example of an object that does everything necessary:

class Forms::Registration

  # ActiveModel plumbing to make `form_for` work
  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  def persisted?
    false
  end

  # Custom application code

  ATTRIBUTES = [:name, :email, :terms_of_service, :anti_bot_question_id, :anti_bot_answer, :ip_address]

  attr_accessor *ATTRIBUTES

  def initialize(attributes = {})
    ATTRIBUTES.each do |attribute|
      send("#{attribute}=", attributes[attribute])
    end
  end

  validates :terms_of_service, :acceptance => true

  validate do
    if anti_bot_answer != anti_bot_question.answer
      errors[:anti_bot_answer] << "Incorrect answer - are you a bot?"
    end
  end

  validate do
    [user, business, membership].each do |object|
      unless object.valid?
        object.errors.each do |key, values|
          errors[key] = values
        end
      end
    end
  end

  def anti_bot_question
    if anti_bot_question_id
      AntiBotQuestion.find_by_id(anti_bot_question_id)
    else
      AntiBotQuestion.random
    end
  end

  def user
    @user ||= User.new(:email => email)
  end

  def business
    @business ||= Business.new(:name => name)
  end

  def membership
    @membership ||= business.memberships.build(:user => user)
  end

  def save
    return false unless valid?
    if create_objects
      UserMailer.deliver_user_registered(user)
      IpLogger.log(user, ip_address)
    else
      false
    end
  end

  private

  def create_objects
    ActiveRecord::Base.transaction do
      user.save!
      business.save!
      membership.save!
    end
  rescue
    false
  end

end

In my opinion this type of form-backing object combines the best of all worlds – it keeps the controller skinny and the
view simple but it does’t pollute the domain at all.

Testing form-backing objects

Form-backing objects are just plain ruby objects, so testing them is very straightforward with unit tests. The only
thing that you probably want to do is make sure that your form-backing object is compatible with form_for by using
the ActiveModel::Lint::Tests. With Test::Unit it’s as simple as:

# http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/

class RegistrationTest < ActiveModel::TestCase
  include ActiveModel::Lint::Tests

  def setup
    @model = Forms::Registration.new
  end
end

If you prefer RSpec, you can easily add a shared example that calls through to ActiveModel::Lint::Tests like so:

# Taken from http://blog.pivotal.io/users/mgehard/blog/articles/making-sure-you-implement-the-activemodel-interface-fully
#
# spec/
# ├── spec_helper.rb
# └── support
#     └── shared_examples
#         └── active_model.rb

shared_examples_for "ActiveModel" do
  require 'test/unit/assertions'
  require 'active_model/lint'
  include Test::Unit::Assertions
  include ActiveModel::Lint::Tests

  before do
    @model = subject
  end

  ActiveModel::Lint::Tests.public_instance_methods.map { |method| method.to_s }.grep(/^test/).each do |method|
    example(method.gsub('_', ' ')) { send method }
  end
end

Once that’s in place, your spec is pretty simple:

require 'spec_helper'

describe Forms::Registration do

  # if your form backing object has required parameters
  # you can add them by overriding `subject` like so:
  #
  # let(:subject) { Forms::Registration.new(:email => 'some@email.com') }

  it_behaves_like "ActiveModel"

end

Staying Dry

If you have a lot of these form-backing objects, you can easily move all of that plumbing to a base class or module. Or
you can use an off-the-shelf gem like Josh Susser’s Informal or James Golick’s
Active Presenter

Comments
  1. grosser says:

    Really like this approach, especially the activemodel part, we used something similar before, but the activemodel-ness was missing, and would have made it much more readable.

    I think the upper 10 lines or so should be extracted into something like ‘FormBacker’, maybe get a bit more meta-magic there and support belongs_to :user and also handle the saving logic, then only bussiness-logic would remain, which imo is an even more ideal solution.

  2. Peter Jaros says:

    Amen! Small objects, small responsibilities. Now mock and stub the underlying `User` object in the form object’s test, and you should have a tight, readable test which reads like a set of user story requirements. If not, you may still need to refactor.

    Did you know? Object obesity is the leading cause of death among software projects 3 months and older.

  3. Jeff Dean says:

    @grosser – Checkout the Informal gem and ActivePresenter – they take care of some of the plumbing you mention.

    I don’t use either of those gems, and I prefer to write these objects from scratch in Rails 3 because it’s so simple to write the plumbing, and because almost none of my form-backing objects are similar enough to really be able to use a declarative style without adding a lot of unnecessary complexity.

  4. Joe Van Dyk says:

    monkey_forms at http://github.com/joevandyk/monkey_forms helps make this easier, plus it lets you easily break forms up into multiple pages.

  5. Jeff Dean says:

    Thanks Joe – I just suggested a new form-backing-objects category on [rubytoolbox](http://ruby-toolbox.com/) with monkey_forms and the other two gems I referenced. We’ll see what happens.

  6. Mark Wilden says:

    Awesome stuff. My only quibble is about introducing a new term for an already well-understood concept. Just call it a Presenter.

  7. Jeff Dean says:

    Thanks! I think the term “form-backing object” is becoming more well known outside of Rails, especially in the Spring framework. I think of it as a way to reference a particular subset of object being used in a presenter pattern.

    With current Rails, presenter patterns require 2 sets of objects – form-backing objects tend to do work, and view-presenter objects that take care of formatting, calculations etc… View-presenter objects often need access to the view template in order to have access to link_to and other view helpers, whereas form-backing objects tend to take params and perform a unit of work.

  8. Casey Provost says:

    This is a pretty nice OO solution to a common problem in rails. The only tweak I have is that the following lines don’t seem to belong in save:

    UserMailer.deliver_user_registered(user)
    IpLogger.log(user, ip_address)

    Filling out a registration form from a SOP standpoint has nothing to do with delivering emails or logging. The caveat to logging is if all form entries are logged. This makes testing simpler and keeps the success case in the controller that glues the form, email, log, rendering together.

    I have been using the form object pattern now for a while and it has been working out really great. Really handy as well when handling variations of a form with different requirements.

    Keep up the good work.

  9. Peter Nixey says:

    Great article. Really nice illustration of both the problem and a very clean solution to it. I’d love to see Form objects become part or Rails Core or at least the Rails Way. In the meantime it’s great to see them championed.

Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *