Skip to main content
โšก Calmops

include vs extend in Ruby: Adding Methods to Classes and Objects

Introduction

Ruby’s include and extend are both used to mix module methods into a class, but they add methods at different levels. Understanding the difference is fundamental to Ruby module design and metaprogramming.

The Core Difference

  • include โ€” adds module methods as instance methods of the class
  • extend โ€” adds module methods as class methods (singleton methods on the class object)
module Foo
  def foo
    "heyyyyoooo!"
  end
end

class Bar
  include Foo
end

Bar.new.foo  # => "heyyyyoooo!"  (instance method โ€” works)
Bar.foo      # => NoMethodError  (not a class method)

class Baz
  extend Foo
end

Baz.foo      # => "heyyyyoooo!"  (class method โ€” works)
Baz.new.foo  # => NoMethodError  (not an instance method)

How They Affect the Ancestor Chain

include inserts the module into the class’s ancestor chain, making its methods available to instances:

module Greetable
  def greet
    "Hello from #{self.class}"
  end
end

class Person
  include Greetable
end

puts Person.ancestors.inspect
# => [Person, Greetable, Object, Kernel, BasicObject]

puts Person.new.greet  # => "Hello from Person"

extend adds the module’s methods to the class’s singleton class โ€” they become class-level methods:

module ClassUtils
  def description
    "I am the #{name} class"
  end
end

class Dog
  extend ClassUtils
end

puts Dog.description  # => "I am the Dog class"
puts Dog.ancestors.inspect
# => [Dog, Object, Kernel, BasicObject]  (Greetable not in chain)

extend on an Instance

You can also extend a specific object instance โ€” adding methods only to that object:

module Debuggable
  def debug_info
    "#{self.class}##{object_id}: #{inspect}"
  end
end

user = User.new("Alice")
user.extend(Debuggable)

puts user.debug_info  # => "User#70234...: #<User ...>"
# Other User instances don't have this method

The Classic Pattern: include for Both Instance and Class Methods

A common Ruby pattern uses self.included to add both instance methods and class methods when a module is included:

module Foo
  # Called automatically when Foo is included in a class
  def self.included(base)
    base.extend(ClassMethods)
  end

  # These become CLASS methods
  module ClassMethods
    def bar
      "class method"
    end
  end

  # These become INSTANCE methods
  def foo
    "instance method"
  end
end

class Baz
  include Foo
end

Baz.bar        # => "class method"
Baz.new.foo    # => "instance method"
Baz.foo        # => NoMethodError (not an instance method)
Baz.new.bar    # => NoMethodError (not a class method)

This pattern is used throughout Rails and many Ruby gems.

ActiveSupport::Concern (Rails)

Rails provides ActiveSupport::Concern as a cleaner version of this pattern:

require 'active_support/concern'

module Timestampable
  extend ActiveSupport::Concern

  # Class-level DSL (runs when included)
  included do
    before_save :update_timestamps
    scope :recent, -> { order(updated_at: :desc) }
  end

  # Class methods block
  class_methods do
    def updated_since(time)
      where('updated_at > ?', time)
    end
  end

  # Instance methods (no special block needed)
  def touch!
    update_column(:updated_at, Time.current)
  end

  private

  def update_timestamps
    self.updated_at = Time.current
  end
end

class Article < ApplicationRecord
  include Timestampable
end

Article.recent.limit(10)
Article.updated_since(1.week.ago)
Article.first.touch!

Practical Examples

include: Shared Behavior Across Classes

module Serializable
  def to_json
    vars = instance_variables.each_with_object({}) do |var, hash|
      hash[var.to_s.delete('@')] = instance_variable_get(var)
    end
    vars.to_json
  end

  def to_csv_row
    instance_variables.map { |v| instance_variable_get(v) }.join(',')
  end
end

class User
  include Serializable
  attr_accessor :name, :email

  def initialize(name, email)
    @name  = name
    @email = email
  end
end

class Product
  include Serializable
  attr_accessor :sku, :price

  def initialize(sku, price)
    @sku   = sku
    @price = price
  end
end

puts User.new("Alice", "[email protected]").to_csv_row
# => "Alice,[email protected]"

extend: Class-Level Utilities

module Findable
  def find_by(conditions)
    all.find { |obj| conditions.all? { |k, v| obj.send(k) == v } }
  end

  def where(conditions)
    all.select { |obj| conditions.all? { |k, v| obj.send(k) == v } }
  end
end

class User
  extend Findable

  attr_reader :name, :role

  def initialize(name, role)
    @name = name
    @role = role
  end

  def self.all
    @all ||= []
  end

  def self.create(name, role)
    user = new(name, role)
    all << user
    user
  end
end

User.create("Alice", "admin")
User.create("Bob",   "user")
User.create("Carol", "admin")

puts User.find_by(name: "Alice").role  # => "admin"
puts User.where(role: "admin").map(&:name).inspect
# => ["Alice", "Carol"]

Combining include and extend

module Validatable
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_variable_set(:@validations, [])
  end

  module ClassMethods
    def validates(field, **rules)
      @validations ||= []
      @validations << { field: field, rules: rules }
    end

    def validations
      @validations || []
    end
  end

  def valid?
    errors.empty?
  end

  def errors
    self.class.validations.each_with_object([]) do |v, errs|
      value = send(v[:field])
      errs << "#{v[:field]} can't be blank" if v[:rules][:presence] && value.to_s.strip.empty?
      errs << "#{v[:field]} is too short"   if v[:rules][:min_length] && value.to_s.length < v[:rules][:min_length]
    end
  end
end

class User
  include Validatable

  attr_accessor :name, :email

  validates :name,  presence: true
  validates :email, presence: true, min_length: 5

  def initialize(name, email)
    @name  = name
    @email = email
  end
end

u = User.new("", "a")
puts u.valid?          # => false
puts u.errors.inspect  # => ["name can't be blank", "email is too short"]

Summary

Method Adds methods as Called on Use case
include M Instance methods obj.method Shared behavior for instances
extend M Class methods Klass.method Shared utilities for the class
obj.extend M Singleton methods obj.method Per-object behavior
self.included hook Both (via extend) Both Add instance + class methods together

Resources

Comments