Form-backing objects for fun and profit

May 22, 2011 Pivotal Labs

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 https://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

About the Author

Biography

Previous
Using MySQL foreign keys, procedures and triggers with Rails
Using MySQL foreign keys, procedures and triggers with Rails

When I made the transition from an ASP.NET developer to a Rails developer, one of the biggest changes I not...

Next
Code Monkey
Code Monkey

So I didn't go to whatever was going on in Baltimore this week, but I did do a whole bunch of open source c...

×

Subscribe to our Newsletter

!
Thank you!
Error - something went wrong!