Written February 8, 2008. Tagged Ruby, Ruby on Rails.
My last stab at mincemeat models wasn't very pretty – wrapping different method sets in blocks and folding them.
This is what I've been doing lately instead, to keep fat Rails models manageable.
In Ruby, there is the concept of the load path. Basically, if you require "foo"
, Ruby will first look for "foo.rb" in your current directory, then in each of the directories in the load path.
Rails adds several directories to your load path, such as "#{RAILS_ROOT}/lib"
and "#{RAILS_ROOT}/app/models"
.
Furthermore, Rails does some magic with Module#const_missing
: if you use a module (classes are modules, too, by inheritance) that isn't known, Rails will automatically try to require
a file.
The filename is assumed to be the name of the module. If the module is nested in other modules, any but the right-most module are assumed to be directories. CamelCase is converted to snake_case. So FooBar::Baz
translates to foo_bar/baz.rb
.
This is why you don't have to explicitly require your models or controllers: their directories are in the load path, and their files follow this naming convention.
To break some code out of a model, create a directory under app/models
with the same name as the model, then put your extension files in this directory, and name them according to the convention.
So to extend the User
model, you could create app/models/user/authentication_extension.rb
:
class User
module AuthenticationExtension
def self.included(klass)
klass.instance_eval do
attr_reader :password
validates_presence_of :password_hash
extend ClassMethods
include InstanceMethods
end
end
module ClassMethods
def authenticate(email, password)
# ...
end
end
module InstanceMethods
def password=(value)
# ...
end
end
end
end
Though this file is automatically loaded when you refer to the module, you still have to refer to it, and explicitly include it in the model class. Like so:
class User < ActiveRecord::Base
include AuthenticationExtension
end
The module we defined is User::AuthenticationExtension
, but within the User
class you don't have to fully qualify the name. Note that this means you have to be careful what names you use: if you define a User::Forum
module for code related to a forum system, and also have a Forum
model, you will have to use ::Forum
to refer to the latter from within the User
model.
I use the FooExtension
naming scheme to avoid these conflicts.
In addition to including modules, you can also use the same idea to e.g. store a bunch of related constants.
If you, say, find yourself using a lot of raw SQL in a model, you could create a class
class User
module SQL
SOME_QUERY = "#{User.table_name}.foo IS NOT NULL AND ..."
# ...
end
end
and then refer to the constants as e.g. User::SQL::SOME_QUERY
, or just SQL::SOME_QUERY
from within the model.
The same caveat about class name conflicts applies.
As was brought up in comments on my last post on this subject, keep in mind that it is sometimes better to create more models, rather than fattening the ones you have. But if they must be fat, cut them up.