awesomeprogrammer.com

Sharing ideas

Solving problems

Gathering solutions

Exchanging thoughts

Ruby On Rails

PHP

Postgres

Debian & Ubuntu
jQuery & CSS

Extracting Form Objects in Practice

Did you get to the point where your model is over 1k lines longs and moreover stuffed with various concerns? If so – maybe it’s time to think about extracting so called form models (Ruby/Rails community always have fancy names for most simple things :p).

What is a form model? It’s simply a Ruby class (d’oh) that encapsulates logic related to a single operation. Dead simple example that comes to mind is some kind of a sign up process that exists in almost every web application.

How to do it?

If you are using Rails 4 you can simply include ActiveModel::Model in your ruby class to benefit from validations, callbacks and all that useful stuff. There are also alternatives like reform or active_type – personally I really like the second one as it’s really small with no extra dependencies.

So let’s say I have a profile form in my app in which user can change bunch of different stuff. It’s quite a big form, it allows changing many things, it accepts nested attributes for one or more associated models – in general my User model have a lot of logic related to that form. Let’s create new form object using active_type and simply move some logic from one class to another.

app/models/user/profile_form.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Inherit from User class - as we still want underlying activerecord object
class User::ProfileForm < ActiveType::Record[User]
  # instead of attr_accessor you can now declare an 'attribute' and typecast it if needed
  attribute :some_dynamic_attribute, :string

  # active_type have it's own nested-attributes a-like method, but we're inheriting from our User clas
  # so we can simply completely move those declarations without breaking anything
  accepts_nested_attributes_for :something_1, reject_if: proc { |a| a[:config].blank? }, allow_destroy: true
  accepts_nested_attributes_for :something_1, reject_if: proc { |a| a[:name].blank? },   allow_destroy: true

  # no monkey business during profile update
  validate :custom_validation

  # callback related to profile update
  # let's save we're stripping some html tags from user's signature
  before_validation :some_callback

  # maybe rebuild cache, because user changed email?
  after_update :other_callback, if: :email_changed?

  # ...
end

And now in where you used User.find(x) to set a @user for your form_for you can simply use User::ProfileForm.find(x) and it will still work (model_name still points to User so there is need to change params format in your controller).

How is that better?

You moved some code from one class to another one – it this even worth the trouble? I think it is – our main User model in now thinner, we have a class that is responsible for updating user’s profile, we’re no longer polluting user with bunch of logic that happens only in single place in the application. This logic in not carried away every time you fetch a user from your database.

This can be especially useful if you’re doing things like sending emails after updating some important column. Now you have a callback disaster waiting to happen. One day you run a rake task and without even knowing you send a bunch of emails to your users (been there, done that).

Downsides

In given example you moved some validation, it’s cool you there is only one place in application where user can update it’s profile. But if suddenly user will be allowed to update profile in another place and you forget to use proper form object you might end up with inconsistent data. If you’re taking logic from User class you need to make sure if it’s not used in other places in your application.

Gotchas

Watch our for gems that are included in your parent class (of course if you’re using underlying activerecord object). paper_clip for example relies on class name (not model name) when saving attachments so in given example your would have to explicitly set path and url in has_attached_file declaration:

app/models/user.rb
1
2
3
4
5
6
class User < ActiveRecord::Base
  has_attached_file :avatar, styles: { thumb: "50x50>" },
    path: ":rails_root/public/system/users/:attachment/:id_partition/:style/:filename",
    url: "/system/users/:attachment/:id_partition/:style/:filename"
  # ...
end

Comments