Rails model extensions (mincemeat models revisited)

Written . 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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:

1
2
3
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

1
2
3
4
5
6
7
8
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.