Models
Modules
One of the most powerful tools in a Rubyist’s toolbox is the module.
Setup
Get the Blogger project from GitHub and run setup procedures:
1 2 3 4 5 |
|
All existing tests should pass. Optionally, run the tests continuously while developing by running guard
Namespacing
Modules are used to namespace Ruby classes. For example, if we had this code:
1 2 3 4 |
|
The class Hello
would be wrapped in the Sample
namespace. That means when we want to create an instance of Hello
instead of:
1
|
|
We would prefix it with the namespace and two colons:
1
|
|
The primary purpose of namespacing classes is to avoid collision. If I write a library that has an Asset
class, for instance, and you use my library in a program which already has an Asset
class, the two class definitions will merge into one Frankenstein class, usually causing unexpected behavior.
If, instead, I wrap mine into a namespace Packager
, then my Packager::Asset
in the library and your Asset
class can happily coexist.
In Rails
Modules can be used to namespace a group of related Rails models:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Typically the classes would be stored in a subfolder of models with the name of the namespace, so here app/models/packager/*.rb
Common Code
The more common usage of modules in Rails is to share common code. These sometimes go by the nickname "mix-ins", but that just means modules.
Inheritance, Modules, and Rails
Ruby implements a single inheritance model, so a given class can only inherit from one other class. Sometimes, though, it’d be great to inherit from two classes. Modules can cover that need.
In ActiveRecord
, inheritance leads into Single Table Inheritance (STI). STI sounds like a good idea, then you end up ripping it out as the project matures. It just isn’t a strong design practice.
Instead, we can mimic inheritance using modules and allow each model to have its own table.
Instance Methods
Let’s look at a scenario where two classes could share an instance method. You’ll find these methods implemented in the sample project’s models:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
The word_count
method is obviously repeated verbatim. We could imagine that, in the future, we might want to modify the word count method so it doesn’t include "a", "and", "or", etc. Or we want to pass in a word and have it tell us how many times that word appears in the document.
These changes will mean changing the same code in two places, and that’s a recipe for regression bugs. Instead, we extract the common code.
Creating the Module
First, we define the module. It can live in /app/models
or another subfolder if you prefer:
1 2 3 4 5 6 |
|
Then make use of the module from the two classes:
1 2 3 4 5 6 7 |
|
The include
method adds any methods in the module as instance methods to the class. Nothing about the usage of article.word_count
would change.
Class Methods
You can use modules to share class methods, too. Starting with similar models:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
We want to extract the common code into a module, but it has to add a class method, not an instance method like the module we wrote before. There are two approaches.
Using a Dedicated Module
1 2 3 4 5 |
|
Note that we’ve removed the self.
from the method definition. Then mix it into the original classes using extend
:
1 2 3 4 5 6 7 8 9 |
|
Previously we used include
to add the module methods as instance methods. Here, we use extend
to add the methods in the module as class methods to the extending class. Our functionality, like Article.total_word_count
would be the same.
Sharing a Module
These two modules are really related to the same domain concept, so let’s figure out how to implement the same functionality with just one module.
Class Methods in the Module
We’d be tempted to add it as a class method to the module:
1 2 3 4 5 6 7 8 9 |
|
Then in the models…
1 2 3 4 5 6 7 |
|
But this won’t work. The self.total_word_count
is defined on the module, not on the including class. We need to do more in the module.
The self.included
Method
Our module can define a self.included
method which will be automatically triggered when the module is included into a class. It usually looks like this:
1 2 3 4 5 |
|
The parameter including_class
is a reference to the class which is including this module.
How does this help us? To define our class methods previously we used extend
. With the included
method, we have a reference to the including class so we can tell that class to extend
our class methods.
But extend
expects a module. We can wrap our class methods into a nested module like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
Then add in the self.included
method which will tell the including class to extend
itself with the ClassMethods
module.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Now, if you try this with the Article
and Comment
, you’ll find that word_count
is correctly added as an instance method while total_word_count
is added as a class method – all by just calling include
.
More on self.included
We can use included
to share more than method definitions. Imagine that both Article
and Comment
share an association with one ModeratorApproval
:
1 2 3 4 5 6 7 8 9 |
|
You decide that all objects in the system that implement TextContent
will also have this relationship. You could then pull it into the module.
A First Attempt
Your first instinct might be to try this:
1 2 3 4 |
|
This won’t work because it’s trying to call a class method has_one
on the module, but that method lives in ActiveRecord
. We really want to call the method on the including class, not the module.
Using self.included
and .send
We can modify our self.included
like so:
1 2 3 4 5 6 7 8 |
|
Not familiar with .send
? It allows us to trigger a private method inside another object. If we just called including_class.extend
here, Ruby would complain. But send
will work just fine.
The send
call is tricky to write, though. Whenever Ruby feels tricky, there must be another way.
Using self.included
and class_eval
When self.included
is making multiple calls or you’re doing something more complex, flip over to using .class_eval
:
1 2 3 4 5 6 7 8 9 10 |
|
Whatever you have inside the class_eval
block will be executed as though it were typed right in the including class’ source.
ActiveSupport::Concern
Rails 3 introduced a module named ActiveSupport::Concern
which has the goal of simplifying the syntax of our modules. Not everyone loves it, but you should at least understand how it works.
To demonstrate its usage, let’s refactor the TextContent
module above.
Setup
First, just inside the module opening, we extend
the helper:
1 2 3 4 |
|
Now ActiveSupport::Concern
is activated.
included
In the original, we define the method callback self.included(including_class)
. With ActiveSupport::Concern
, instead we call a class method on the module itself named included
:
1 2 3 4 5 6 7 8 |
|
The block passed to included
is executed as though it were in a class_eval
.
Interior Modules
If our module follows the pattern of defining class methods in an interior module named ClassMethods
, ActiveSupport::Concern
will automatically extend
the including class with that module. So we can omit the call to extend
in our included
method:
1 2 3 4 5 6 7 |
|
Similarly, if you have an interior module named InstanceMethods
, included
will automatically call include
on the including class and pass in that module.
Completed Refactoring
So, in the end, we have:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Then, in the models:
1 2 3 4 5 6 7 |
|
ActiveSupport::Concern
allowed us to save a few lines of "boilerplate" code in the module.
Exercises
- Define the
TextContent
module as described above. - Include the module into both
Comment
andArticle
models. - Pull the related tests out of
article_spec.rb
andcomment_spec.rb
, write atext_content_spec.rb
, and relocate the tests. Now that you’ve ensured the functionality of the methods, from thearticle_spec.rb
andcomment_spec.rb
you can just check that the class and instances respond to the proper methods. - Define a second module named
Commentable
that, for starters, just causes the including class to runhas_many :comments
. Remove thehas_many
fromArticle
and, instead, include the module. Imagine that, in the future, we’d have aPhoto
object which also accepted comments. - Define an instance method in the
Commentable
module namedhas_comments?
which returns true or false based on the existence of comments. In thearticles#show
view, use that method to show or hide the comments display based on their existence.