As projects grow in size the models tend to grow in complexity. Let’s look at a few strategies for managing the situation.
The core issue is that
ActiveRecord classes mix two roles: persistence and business logic. This is convenient, especially when first starting an application, but it violates the "Single Responsibility Principle."
As a project matures there becomes a clearer division between these roles, and breaking them up into distinct domain objects is often a good idea.
Creating a Processor Object
A processor object is concerned only with manipulating the data from other objects, it has no persistence itself. It might be an implementation of the Facade Pattern or even a kind of Decorator Pattern.
Writing a processor is very easy:
1 2 3 4 5 6
It’s just a "PORO" or "Plain Old Ruby Object".
Where Does It Live?
You can store your processor objects into
app/models, but if you’d like a little more separation it’s common to create
app/lib and store them in there. Any folder added under
app/ will be added to the automatic load path when the server starts, so create folders whenever they make sense for the organization of your project.
A processor object will primarily use the same Ruby techniques you’re accustomed to, but here are a few methods that will make life easier:
attr_reader, short for "Attribute Reader", creates an instance variable and accessor method for you:
1 2 3 4 5 6 7 8 9
So you’re creating an attribute that, from outside the instance, can only be read. If you wanted to allow write access you’d use
attr_accessor, though that’s probably violating the encapsulation of the child objects.
If you are creating multiple attributes you can combine them in one call to
attr_reader like this:
The Law of Demeter says, generally speaking, that we can talk to an object but shouldn’t talk directly to the object’s children.
For instance, imagine we have a
Plane instance in
@plane. We want the engines started. The temptation is to write something like this:
But that assumes knowledge of how
@plane relates to its engines. What if there’s only a single engine? Will there still be an
engines method that returns a collection, or will there only be
engine? We’re breaking the encapsulation of the plane class.
Instead, proper object oriented design would be to tell the plane what to do:
That leaves it up to the
@plane to decide what it means to start the engines.
How does this relate to processor objects? When you create a facade, you’ll often want to act on attributes and methods of the child objects. Don’t do this:
How do you make that work? Here’s the simplistic approach:
1 2 3 4 5 6 7
If you have multiple child objects with many methods, writing and maintaining these proxy methods will be a pain. Instead, use
1 2 3 4
This has the exact same effect as the wrapper above. You can delegate many methods at once:
1 2 3 4
Now you can preserve encapsulation but have easily maintained proxies.
Imagine we’re writing a reporting system for a school. We want to follow a REST pattern, and our top-down design says that we should access a
Report resource. We’ll calculate the data on the fly, so it isn’t necessary to store anything about the report in the database. So we start the
Report class like this:
The report is going to mix an instance of
1 2 3
That will setup
@report_type instance variables as well as the similarly named accessor methods.
From there we could expose child attributes:
1 2 3 4 5 6
And from the outside they can be accessed, preserving encapsulation, like
Then the facade can do work with the child objects:
1 2 3 4 5 6 7 8
Use the Blogger sample application to complete the exercises in this section. See the Setup Instructions for help.
We have both
Comment models. Let’s imagine that we want to start running some statistics on them. For instance, we want to know how many total words are in the articles and its child comments.
- Implement a
ContentThreadprocessor object that wraps both an article and the set of comments.
- Implement a
word_countmethod that calculates the total word count of the article and all comments.
- Proxy the
titlemethod so when it is called on an instance of
ContentThreadit returns the title of the article.
- Create a
commentorsmethod that fetches all the comment authors.
- Create a
last_updatedmethod that returns the most recent change to the thread, either a change to the article or to a comment.