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 classextendโ 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
- Ruby Docs: Module#include
- Ruby Docs: Object#extend
- Rails Tips: Include vs Extend
- ActiveSupport::Concern
Comments