How to split a Ruby class into multiple source files

Many Rails projects have one or two models that act as a hub for the rest of the application. One is usually the User model, the others depend on your problem domain. If your application is about managing projects, chances are the Project class does a lot of things.

Such larger models are likely concerned with multiple themes like "permission checks" or "budget calculations". Each of these concerns requires a couple of validations and callbacks here, and a method definition there.

Lumping all these concerns into one large file blurs the lines between them. Someone opening the Project class for the first time will have a hard time seeing its individual aspects. In addition, a developer will usually be interested in a single concern in order to fix a bug or implement a new feature.

A way to better organize a large class is to split it into multiple source files. Ruby comes with a built-in tool for that: modules. Unfortunately vanilla Ruby modules lack support for many idioms popular in modern Ruby. Most importantly, we have become accustomed to composing our classes with meta-programming macros such as has_many, validates_presence_of or after_save. And modules weren't built with macros in mind.

Enter Modularity. Modularity is to your models what partials are for your views. It lets you write classes like this:

class Project
  does 'project/billing'
  does 'project/budgets'
  does 'project/permissions'
end

This Project class is now organized in three additional sources files called traits:

app/models/project.rb
app/models/project/billing_trait.rb
app/models/project/budgets_trait.rb
app/models/project/permissions_trait.rb

Each trait is packaging a single concern, including its method definitions and macro calls. Let's take a look at the trait that deals with project billing:

module Project::BillingTrait
  as_trait do

    has_defaults :billed => false
    belongs_to :billing_address_id
    validates_presence_of :bill_date, :if => :billed?

    def billed?
      billing_address_id.present?
    end

    def payment_overdue?
      billed? && bill_date + 21.days < Time.now 
    end

  end
end

Organizing a model into partial classes is only one of Modularity's many uses. Most importantly you can parametrize your traits. Just like has_many :cats and has_many :dogs will add different methods to your class, a parametrized trait can provide different behavior depending on which argument they are included with.

Find out more about rolling your own meta-programming macros with Modularity over at Agile Web Operations.

You can follow any response to this post through the Atom feed.

Avatar

Sat, 17 Apr 2010 18:59:00 GMT

by henning

Tags:

  • James leukin said about 1 month later:

    This seems pretty similar to dm is remixable? Does this work outside the scope of rails?

  • Henning said about 1 month later:

    dm-is-remixable could be expressed in a Modularity trait.

    You can use Modularity in any Ruby application, Rails is not required.

  • Ryan Bigg said about 1 month later:

    What is the difference between doing this and using the standard “include” method Ruby provides? Is it just syntatic sugar for the sake of it or do I get a free rainbow with it?

  • Henning said about 1 month later:

    @Ryan: Your free rainbow is that modularity traits can be parametrized, providing different behavior depending on which arguments they are included with. This is not possible with Ruby’s include.

    Check out the link at the end of the article for an example.

Leave a comment