Here’s a neat trick I learned today.
Typically, when a class includes a module, the module methods become instance methods on the including class. However, with just a little metaprogramming, a module can provide both instance and class methods to the including class. Here’s how that works.
module Taggable
def self.included(base)
puts "Taggable is included in #{base}..extending it now."
base.extend(ClassMethods)
end
def save
puts "instance method: Saving the post"
end
module ClassMethods
def find(id)
puts "class method: Finding post with the id #{id}"
end
end
end
class Post
include Taggable
end
# Usage
Post.find(3)
post = Post.new
post.save
# Output
# Taggable is included in Post..extending it now.
# class method: Finding post with the id 3
# instance method: Saving the post
Here, save
is a regular method in the Taggable
module that becomes an instance method for Post
. On the other hand, find
is a method defined in the Taggable::ClassMethods
module, that becomes a class method on the Post
class.
This works because Ruby provides a hook method Module.included(base)
that is invoked whenever another module or a class includes the module containing the above method.
module A
def A.included(mod)
puts "#{self} included in #{mod}"
end
end
module Enumerable
include A
end
# prints "A included in Enumerable"
Now, when Post
class includes Taggable
module, included
is invoked, and the Post
class is passed as the base
argument. Then, we extend Post
with ClassMethods
, which adds the methods defined in the ClassMethods
as the class methods on the Post
class.
def self.included(base)
puts "Taggable is included in #{base}..extending it now."
base.extend(ClassMethods)
end
As a result, Post
gets both instance methods like save
and class methods like find
.
This include-and-extend trick gives you a nice way to structure the library. With a single include
, your class gets access to instance and class methods defined in a well-isolated module. Rails heavily used this pattern, which was later encoded into a feature called concerns, which I will explore in a future post.