Skip to main content
โšก Calmops

Ruby Class Extension Pattern: Adding Class Methods via Modules

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

Resources

Comments