cult3

Using Form Objects in Ruby on Rails

Nov 04, 2015

Table of contents:

  1. Why is this a problem?
  2. So what validations should go in the model?
  3. What is a Form Object
  4. Creating a Form Object
  5. Conclusion

Over the last couple of weeks, we’ve looked at two integral aspects of Ruby on Rails.

First, Models form the backbone of many Rails projects as Ruby on Rails has a really good implementation of Active Record. We looked at Models in Getting started with Active Record in Ruby on Rails.

Last week we looked at defining validations for models in Working with Validation in Ruby on Rails. Validation is extremely important because you must protect the integrity of the data that enters your application.

However, whilst these two aspects are very important, they can also lead to the downfall of many Rails projects.

When you crack open a Rails model such as the User model, you will likely encounter a huge file.

Model objects tend to attract a lot of functionality. It’s not surprising to see a whole load of validation definitions for every situation the model is used in throughout the application.

To prevent this from happening, we can create Form Objects that deal with special use cases. By encapsulating the logic in a Form Object, we can remove the burden from the Model.

In today’s tutorial we will be looking at creating Form Objects.

Why is this a problem?

In a typical web application, you will usually find that there are 2 or 3 models that end up with a lot of responsibility. These models are used throughout the application and under numerous different circumstances.

For example, in a social web application, the User model will likely be used during signup, changing passwords, updating profile details, connecting with other users, as well as a whole load more other things.

A process such as signing up a new user will usually have specific things that need validating. For example, perhaps you require the user to repeat their password as confirmation, or check a “terms and conditions” box before they can create an account.

These extra validations should not be the concern of the User model, yet you often find validation definitions in a monolithic model that are only for a single purpose.

A better way to deal with these one off processes is to encapsulate the logic in a Form Object.

This means you don’t have to clutter the model with extra validations and you make testing that single process a lot easier.

This also makes is much easier to deal with actions that require you to use multiple models in one action. Instead of trying to cram them both into one model, you can simply use a Form Object.

So what validations should go in the model?

When you learn of a new technique to extract functionality into an abstraction, it’s tempting to go overboard. I’ve definitely been guilty of that in the past.

Just because you can extract validation logic for specific processes, does not mean your model object should have no validation at all.

The validation in the model should be the core business rules that protect against invalid data.

So for example, it’s probably important that each of your users uses a unique email address. This validation should remain on your User model.

A model with no validation definitions is just as a bad design as a one with hundreds.

As a new developer on the project, you should be able to open the model and get a good sense of what data attributes are important, without getting overwhelmed with every validation rule for every possible scenario in which the model will be used.

What is a Form Object

A Form Object is basically just an object that we can be used in a form as a replacement for an ActiveRecord object.

The Form Object should quack like an ActiveRecord object. This means the object should respond to messages in the same way as an ActiveRecord object would.

The Form Object will be a simple Ruby Object, however, we can include Virtus, a gem which gives plain Ruby objects super powers.

The Form Object will encapsulate the logic around validating incoming data, and taking the appropriate action.

Creating a Form Object

As an example of creating a Form Object, lets imagine we need to deal with new registrations for our SaaS application.

As part of the registration process we need to create a Company and a User.

We will gather some basic details, and we need to ensure the terms and conditions have been accepted.

So first up, we can define the Form Object:

class RegistrationForm
  include Virtus

  extend ActiveModel::Naming
  include ActiveModel::Conversion
  include ActiveModel::Validations

  attribute :name, String
  attribute :company_name, String
  attribute :email, String
  attribute :terms, Boolean

  attr_reader :user
  attr_reader :company

  validates :name, presence: true
  validates :company_name, presence: true
  validates :email, presence: true
  validates :terms, acceptance: true

  def persisted?
    false
  end
end

Here is the basic blueprint for a Form Object. As you can see, we have include Virtus as well as some of the building blocks of ActiveModel.

I’ve also defined the attributes of the Form Object as well as the validation rules that will determine if the data is considered valid.

And finally we need to implement the persisted? method so that this object quacks like an ActiveRecord object.

Next we can define the save method:

def save
  if valid?
    # Logic for creating a Company and a User
    true
  else
    false
  end
end

If the form data is valid we can deal with the logic of creating a company and a new user. Otherwise we can return false. This allows you to create multiple models without having to crowbar the functionality into one or both of the ActiveRecord objects.

You will now be able to use this Form Object instead of the ActiveRecord object when passing data to and from the View and the Controller.

Conclusion

Validation is very important, but there’s a difference between model validation and process validation.

You should avoid adding process validation to a model because it will make the model bloated and brittle. Instead, you can abstract that logic into a Form Object.

A Form Object will encapsulate the logic around the process. This is useful for one off validation rules or when the process should create multiple models.

The Form Object can be tested in isolation and will remove the clutter from model. This means your models won’t get bloated with one-off brittle validation definitions!

Philip Brown

@philipbrown

© Yellow Flag Ltd 2024.