The Pug Automatic

Rails model extensions (mincemeat models revisited)

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.

Background

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.

Extensions

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.

Constant repositories

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.

In closing

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.