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 | |
The 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
TextContentmodule as described above. - Include the module into both
CommentandArticlemodels. - Pull the related tests out of
article_spec.rbandcomment_spec.rb, write atext_content_spec.rb, and relocate the tests. Now that you’ve ensured the functionality of the methods, from thearticle_spec.rbandcomment_spec.rbyou can just check that the class and instances respond to the proper methods. - Define a second module named
Commentablethat, for starters, just causes the including class to runhas_many :comments. Remove thehas_manyfromArticleand, instead, include the module. Imagine that, in the future, we’d have aPhotoobject which also accepted comments. - Define an instance method in the
Commentablemodule namedhas_comments?which returns true or false based on the existence of comments. In thearticles#showview, use that method to show or hide the comments display based on their existence.