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.