Introduction
When you include a module in Ruby, the module’s methods become instance methods of the including class. But what if you want the module to also add class methods? The classic solution is the self.included hook combined with extend(ClassMethods) โ a pattern used extensively in Rails and many Ruby gems.
The Problem
include only adds instance methods:
module Greetable
def hello
"Hello from #{self.class}"
end
def self.version
"1.0" # This is a module method โ NOT added to the including class
end
end
class Person
include Greetable
end
Person.new.hello # => "Hello from Person" โ
Person.version # => NoMethodError โ
Greetable.version # => "1.0" (only accessible on the module itself)
The Solution: self.included + extend
The self.included(base) hook is called automatically when a module is included. base is the class doing the including. By calling base.extend(ClassMethods), you add the nested module’s methods as class methods:
module M
# Called automatically when M is included in a class
def self.included(base)
base.extend(ClassMethods)
end
# These become CLASS methods of the including class
module ClassMethods
def my_class_method
"I am a class method on #{self}"
end
end
# These become INSTANCE methods of the including class
def my_instance_method
"I am an instance method on #{self.class}"
end
end
class C
include M
end
C.my_class_method # => "I am a class method on C"
C.new.my_instance_method # => "I am an instance method on C"
A Real-World Example: Validatable Module
module Validatable
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@validations, [])
end
module ClassMethods
def validates(field, **options)
@validations ||= []
@validations << { field: field, options: options }
end
def validations
@validations || []
end
end
def valid?
self.class.validations.all? do |v|
value = send(v[:field])
if v[:options][:presence]
!value.nil? && !value.to_s.strip.empty?
elsif v[:options][:min_length]
value.to_s.length >= v[:options][:min_length]
else
true
end
end
end
def errors
self.class.validations.each_with_object([]) do |v, errs|
value = send(v[:field])
if v[:options][:presence] && (value.nil? || value.to_s.strip.empty?)
errs << "#{v[:field]} can't be blank"
elsif v[:options][:min_length] && value.to_s.length < v[:options][:min_length]
errs << "#{v[:field]} is too short (minimum #{v[:options][:min_length]} characters)"
end
end
end
end
class User
include Validatable
attr_accessor :name, :email, :password
validates :name, presence: true
validates :email, presence: true
validates :password, min_length: 8
def initialize(name, email, password)
@name = name
@email = email
@password = password
end
end
user = User.new("Alice", "[email protected]", "secret")
puts user.valid? # => false
puts user.errors.inspect
# => ["password is too short (minimum 8 characters)"]
user2 = User.new("Bob", "[email protected]", "securepassword")
puts user2.valid? # => true
The ActiveSupport::Concern Alternative
Rails provides ActiveSupport::Concern which makes this pattern cleaner:
require 'active_support/concern'
module Timestampable
extend ActiveSupport::Concern
# Runs when module is included โ like self.included
included do
before_create :set_created_at
before_save :set_updated_at
end
# Class methods block โ no need for nested ClassMethods module
class_methods do
def recent(limit = 10)
order(created_at: :desc).limit(limit)
end
def created_after(date)
where('created_at > ?', date)
end
end
# Instance methods (no special block needed)
def age_in_days
(Time.now - created_at) / 86400
end
private
def set_created_at
self.created_at ||= Time.now
end
def set_updated_at
self.updated_at = Time.now
end
end
class Article < ApplicationRecord
include Timestampable
end
Article.recent(5)
Article.created_after(1.week.ago)
Article.first.age_in_days
Comparing the Patterns
# Pattern 1: Manual self.included + ClassMethods
module MyModule
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def class_method; end
end
def instance_method; end
end
# Pattern 2: ActiveSupport::Concern (Rails)
module MyModule
extend ActiveSupport::Concern
class_methods do
def class_method; end
end
def instance_method; end
end
# Pattern 3: extend + include together (without a hook)
module ClassMethods
def class_method; end
end
module InstanceMethods
def instance_method; end
end
class MyClass
extend ClassMethods
include InstanceMethods
end
When to Use Each
| Pattern | Use When |
|---|---|
self.included + ClassMethods |
Plain Ruby (no Rails dependency) |
ActiveSupport::Concern |
Rails projects โ cleaner syntax, handles dependencies |
Separate extend + include |
Simple cases, explicit is fine |
Chaining Concerns
ActiveSupport::Concern handles dependency chains automatically:
module Auditable
extend ActiveSupport::Concern
include Timestampable # Concern can include other concerns
class_methods do
def audit_log
where(audited: true)
end
end
def audit!
update(audited: true, audited_at: Time.now)
end
end
class Order < ApplicationRecord
include Auditable # automatically includes Timestampable too
end
Comments