Include-and-Extend
September 25, 2021 in ruby

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.