- 9.1 Scopes
- 9.2 Callbacks
- 9.3 Calculation Methods
- 9.4 Single-Table Inheritance (STI)
- 9.5 Abstract Base Model Classes
- 9.6 Polymorphic has_many Relationships
- 9.7 Enums
- 9.8 Foreign-Key Constraints
- 9.9 Modules for Reusing Common Behavior
- 9.10 Modifying Active Record Classes at Runtime
- 9.11 Using Value Objects
- 9.12 Nonpersisted Models
- 9.13 PostgreSQL Enhancements
- 9.14 Conclusion
9.9 Modules for Reusing Common Behavior
In this section, we’ll talk about one strategy for breaking out functionality that is shared between disparate model classes. Instead of using inheritance, we’ll put the shared code into modules.
In the section “Polymorphic has_many Relationships” in this chapter, we described how to add a commenting feature to our recurring sample “Time and Expenses” application. We’ll continue fleshing out that example, since it lends itself to factoring out into modules.
The requirements we’ll implement are as follows: Both users and approvers should be able to add their comments to a Timesheet or ExpenseReport. Also, since comments are indicators that a timesheet or expense report requires extra scrutiny or processing time, administrators of the application should be able to easily view a list of recent comments. Human nature being what it is, administrators occasionally gloss over the comments without actually reading them, so the requirements specify that a mechanism should be provided for marking comments as “OK” first by the approver and then by the administrator.
Again, here is the polymorphic has_many :comments, as: :commentable that we used as the foundation for this functionality:
1 class Timesheet < ActiveRecord::Base
2 has_many :comments, as: :commentable
3 end
4
5 class ExpenseReport < ActiveRecord::Base
6 has_many :comments, as: :commentable
7 end
8
9 class Comment < ActiveRecord::Base
10 belongs_to :commentable, polymorphic: true
11 end
Next we enable the controller and action for the administrator that list the 10 most recent comments with links to the item to which they are attached.
1 class Comment < ActiveRecord::Base
2 scope :recent, -> { order('created_at desc').limit(10) }
3 end
4
5 class CommentsController < ApplicationController
6 before_action :require_admin, only: :recent
7 expose(:recent_comments) { Comment.recent }
8 end
Here’s some of the simple view template used to display the recent comments:
1 %ul.recent.comments
2 - recent_comments.each do |comment|
3 %li.comment
4 %h4= comment.created_at
5 = comment.text
6 .meta
7 Comment on:
8 = link_to comment.commentable.title, comment.commentable
9 # Yes, this would result in N+1 selects.
So far, so good. The polymorphic association makes it easy to access all types of comments in one listing. In order to find all the unreviewed comments for an item, we can use a named scope on the Comment class together with the comments association.
1 class Comment < ActiveRecord::Base
2 scope :unreviewed, -> { where(reviewed: false) }
3 end
4
5 >> timesheet.comments.unreviewed
Both Timesheet and ExpenseReport currently have identical has_many methods for comments. Essentially, they both share a common interface. They’re commentable!
To minimize duplication, we could specify common interfaces that share code in Ruby by including a module in each of those classes, where the module contains the code common to all implementations of the common interface. So, mostly for the sake of example, let’s go ahead and define a Commentable module to do just that and include it in our model classes:
1 module Commentable
2 has_many :comments, as: :commentable
3 end
4
5 class Timesheet < ActiveRecord::Base
6 include Commentable
7 end
8
9 class ExpenseReport < ActiveRecord::Base
10 include Commentable
11 end
Whoops, this code doesn’t work! To fix it, we need to understand an essential aspect of the way that Ruby interprets our code dealing with open classes.
9.9.1 A Review of Class Scope and Contexts
In many other interpreted, object-oriented programming languages, you have two phases of execution: one in which the interpreter loads the class definitions and says, “This is the definition of what I have to work with,” and the second in which it executes the code. This makes it difficult (though not necessarily impossible) to add new methods to a class dynamically during execution.
In contrast, Ruby lets you add methods to a class at any time. In Ruby, when you type class MyClass, you’re doing more than simply telling the interpreter to define a class; you’re telling it to “execute the following code in the scope of this class.”
Let’s say you have the following Ruby script:
1 class Foo < ActiveRecord::Base
2 has_many :bars
3 end
4 class Foo < ActiveRecord::Base
5 belongs_to :spam
6 end
When the interpreter gets to line 1, we are telling it to execute the following code (up to the matching end) in the context of the Foo class object. Because the Foo class object doesn’t exist yet, it goes ahead and creates the class. At line 2, we execute the statement has_many :bars in the context of the Foo class object. Whatever the has_many method does, it does right now.
When we again say class Foo at line 4, we are once again telling the interpreter to execute the following code in the context of the Foo class object, but this time, the interpreter already knows about class Foo; it doesn’t actually create another class. Therefore, on line 5, we are simply telling the interpreter to execute the belongs_to :spam statement in the context of that same Foo class object.
In order to execute the has_many and belongs_to statements, those methods need to exist in the context in which they are executed. Because these are defined as class methods in ActiveRecord::Base, and we have previously defined class Foo as extending ActiveRecord::Base, the code will execute without a problem.
However, let’s say we defined our Commentable module like this:
1 module Commentable
2 has_many :comments, as: :commentable
3 end
In this case, we get an error when it tries to execute the has_many statement. That’s because the has_many method is not defined in the context of the Commentable module object.
Given what we now know about how Ruby is interpreting the code, we now realize that what we really want is for that has_many statement to be executed in the context of the including class.
9.9.2 The included Callback
Luckily, Ruby’s Module class defines a handy callback that we can use to do just that. If a Module object defines the method included, it gets run whenever that module is included in another module or class. The argument passed to this method is the module/class object into which this module is being included.
We can define an included method on our Commentable module object so that it executes the has_many statement in the context of the including class (Timesheet, ExpenseReport, etc.):
1 module Commentable
2 def self.included(base)
3 base.class_eval do
4 has_many :comments, as: :commentable
5 end
6 end
7 end
Now when we include the Commentable module in our model classes, it will execute the has_many statement just as if we had typed it into each of those classes’ bodies.
The technique is common enough, within Rails and gems, that it was added as a first-class concept in the Active Support API as of Rails 3. The previous example becomes shorter and easier to read as a result:
1 # app/models/concerns/commentable.rb
2 module Commentable
3 extend ActiveSupport::Concern
4 included do
5 has_many :comments, as: :commentable
6 end
7 end
Whatever is inside of the included block will get executed in the class context of the class where the module is included.
As of version 4.0, Rails includes the directory app/models/concerns as a place to keep all your application’s model concerns. Any file found within this directory will automatically be part of the application load path.