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
# 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
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