Refactor your code with Form Objects

| Comments

I’m pretty sure you have heard about Form Objects, but in case you don’t, let me show you the advantages of using them and a simple way to do that. Why? Because I think a Form Object is like a good diet for your Rails models - it helps to keep it smooth and healthy. With Form Objects you can simply extract all the form-related logic out of the model (or wherever you have it) to make it at least a little bit more SOLID.

Web apps use forms a lot and the Rails applications are not an exception. Sometimes the website form is simple, but often it goes beyond the basics and is much more complicated. Your model is fat, it has tons of validations, dozens of app specific methods and nested attributes out of nowhere. You do not want to touch this model anymore. So…

I bet you know how does the form works in Rails normally, so I’ll omit that part. Just take a look on a very simple Form Object implementation, which you should have been using before reading this post!

The Form class

First of all let’s create a dead simple PORO with a name of UserRegistrationForm. To do that we need a user_registration_form.rb file placed in a ‘forms’ directory. Of course you can name your directory as you wish, ‘forms’ just seems to be quite right for me.

/app/forms/user_registration_form.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class UserRegistrationForm

  include ActiveModel::Model

  attr_accessor :email, :password

  # some validations
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX, message: " - invalid e-mail format." }
  validates :password, presence: true

  validate :email_uniqueness

  def email_uniqueness
    if User.find_by(email: self.email)
      errors.add(:email, " is already taken!")
    end
  end

end

There’s no magic here. Just include ActiveModel::Model to help our class acting like a… model. We set up attr_accessor for the form fields we want to submit and have some validations. You see the first advantage here? The model does not need to validate the form data anymore, yay!

View

To use the form object in the view you simply pass the form object instance variable from the controller to the view. Just like this:

app/views/new.haml
1
2
3
4
  = form_for @user_registration_form, url: users_path, :method => "post" do |f|
    %input {:type => "text", :name => "user_registration_form[email]", :placeholder => "E-mail"}
    %input {:type => "password", :name => "user_registration_form[password]", :placeholder => "Password" }
    %p.submit= f.submit "Submit", data: { disable_with: "Please wait..." }

Controller

To make it work properly we need to instantiate the form object in the controller.

app/controllers/users_controller.rb
1
2
3
  def new
    @user_registration_form = UserRegistrationForm.new
  end

The next thing we need to be done is the ‘create’ action, where we pass the form data with whitelisted params using Rails ‘permit’ method.

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
  def create
    user_registration_form = UserRegistrationForm.new(params[:user_registration_form].permit(:email, :password))
    RegisterUser.new(user_registration_form).call
    flash[:success] = "Yay!"
    redirect_to root_path
  rescue RegisterUser::InvalidFormData
    flash[:danger] = "Too bad!"
    redirect_to :back
  end

What’s next? We want to keep our app in the good condition so we’re calling the RegisterUser Service Object (which I’ve described earlier here by the way) that takes care of the rest. Our controller does not need to know more details. Its responsibility is minimized to handle the requests and rising an exception if something goes wrong. No validations, no persistence, no other responsibilities - only the http-requests-related things. That’s the controller!

Model

Huh…? The only thing that our model needs for now is to exist. All the other form-related things are handled out of the model scope. It’s not a model responsibility to take care of the form data. If you want our model to take care of the forms and validate the data, you’re breaking the SRP rule and to be honest - you can write your whole app in a single file with one big class. It doesn’t matter :-)

TL;DR

Of course you can keep writing your app in “The Rails Way”, but for me it’s much easier when I use form and service objects. The code is clear, the responsibilities are separated, the models are not so fat. I would say that everything is easier to maintain, test and understand. So why not to do that?

Comments