CrackedRuby logo

CrackedRuby

extend vs include

Ruby module inclusion mechanisms that add module methods to classes and objects through different inheritance paths.

Metaprogramming Singleton Classes
5.5.4

Overview

Ruby provides two primary mechanisms for incorporating module functionality: include and extend. Both add module methods to receiving objects, but they differ fundamentally in how and where those methods become available.

When a class includes a module, Ruby inserts the module into the class's ancestor chain above the class itself. This makes the module's instance methods available as instance methods of the class. Every object created from that class can call these methods.

module Greetings
  def hello
    "Hello from #{self.class}"
  end
end

class Person
  include Greetings
end

person = Person.new
person.hello  # => "Hello from Person"

The extend mechanism works differently. When an object extends a module, Ruby adds the module's instance methods directly to that specific object as singleton methods. These methods become available only on the extended object, not on other instances of the same class.

module Greetings
  def hello
    "Hello from #{self.class}"
  end
end

class Person
end

person = Person.new
person.extend(Greetings)
person.hello  # => "Hello from Person"

another_person = Person.new
another_person.hello  # NoMethodError

When extend operates at the class level, it adds the module's methods as class methods rather than instance methods. This happens because classes in Ruby are objects, and extending a class object adds singleton methods to that class object.

class Person
  extend Greetings
end

Person.hello  # => "Hello from Class"

Ruby's method lookup follows a specific path through the ancestor chain. For included modules, Ruby searches the module before searching the class that included it. For extended modules, Ruby searches the singleton class first, where the extended methods reside.

The inheritance hierarchy changes differently with each approach. Including a module modifies the class's ancestor chain, affecting all instances. Extending modifies only the specific object's singleton class, leaving other instances unchanged.

Both mechanisms respect Ruby's method visibility rules. Private and protected methods in modules maintain their visibility when included or extended, though the context in which they operate changes based on the inclusion method.

Basic Usage

Include modules when multiple instances of a class need the same set of methods. This pattern works best for shared behavior that logically belongs to all instances of a class.

module Trackable
  def track_event(event)
    puts "Tracking: #{event} for #{self.class}"
  end
  
  def last_tracked
    @last_event
  end
end

class User
  include Trackable
  
  def initialize(name)
    @name = name
  end
end

class Product
  include Trackable
  
  def initialize(sku)
    @sku = sku
  end
end

user = User.new("Alice")
user.track_event("login")  # Tracking: login for User

product = Product.new("ABC123")
product.track_event("view")  # Tracking: view for Product

The included module methods access instance variables and other instance methods of the including class. The module code executes in the context of the instance that calls it.

module Measurable
  def measure
    @measurements ||= []
  end
  
  def add_measurement(value)
    measure << value
    calculate_average
  end
  
  private
  
  def calculate_average
    @average = measure.sum / measure.size.to_f
  end
end

class Temperature
  include Measurable
  
  def current_average
    @average || 0
  end
end

temp = Temperature.new
temp.add_measurement(72.5)
temp.add_measurement(74.1)
temp.current_average  # => 73.3

Use extend for adding functionality to specific objects without affecting their class or other instances. This works well for object-specific behavior or one-off customizations.

module AdminCapabilities
  def delete_user(user_id)
    "Deleting user #{user_id}"
  end
  
  def view_logs
    "Accessing system logs"
  end
end

regular_user = User.new("Bob")
admin_user = User.new("Carol")

admin_user.extend(AdminCapabilities)
admin_user.delete_user(123)  # => "Deleting user 123"
regular_user.delete_user(123)  # NoMethodError

Class-level extension adds methods as class methods, making them available without instantiating objects. This pattern suits factory methods, configuration, and utility functions.

module Buildable
  def build_from_hash(hash)
    instance = new
    hash.each { |key, value| instance.instance_variable_set("@#{key}", value) }
    instance
  end
  
  def default_attributes
    { status: "active", created_at: Time.now }
  end
end

class Document
  extend Buildable
  
  attr_reader :title, :status, :created_at
  
  def initialize
    self.class.default_attributes.each do |key, value|
      instance_variable_set("@#{key}", value)
    end
  end
end

doc = Document.build_from_hash(title: "Ruby Guide", status: "draft")
doc.title   # => "Ruby Guide"
doc.status  # => "draft"

Advanced Usage

Multiple modules can be included or extended simultaneously, with method lookup following the reverse order of inclusion. The last included module appears first in the ancestor chain.

module A
  def test; "A"; end
end

module B
  def test; "B"; end
end

class Example
  include A
  include B
end

Example.new.test  # => "B"
Example.ancestors  # => [Example, B, A, Object, Kernel, BasicObject]

Modules can include other modules, creating complex hierarchies. When a class includes a module that includes other modules, all modules enter the ancestor chain in the order they were processed.

module Database
  def save
    "Saving to database"
  end
end

module Validations
  include Database
  
  def validate
    return false unless valid?
    save
  end
  
  private
  
  def valid?
    true
  end
end

module Timestamps
  def touch
    @updated_at = Time.now
  end
end

class Record
  include Validations
  include Timestamps
end

Record.ancestors
# => [Record, Timestamps, Validations, Database, Object, Kernel, BasicObject]

Dynamic extension allows runtime modification of objects based on conditions or user input. This technique enables flexible object behavior without subclassing.

module JsonSerializer
  def to_json
    instance_variables.each_with_object({}) do |var, hash|
      key = var.to_s.delete("@")
      hash[key] = instance_variable_get(var)
    end.to_json
  end
end

module XmlSerializer
  def to_xml
    "<record>#{instance_variables.map { |v| 
      key = v.to_s.delete("@")
      value = instance_variable_get(v)
      "<#{key}>#{value}</#{key}>"
    }.join}</record>"
  end
end

class DataRecord
  def initialize(format = :json)
    @data = "sample"
    case format
    when :json then extend JsonSerializer
    when :xml then extend XmlSerializer
    end
  end
end

json_record = DataRecord.new(:json)
json_record.to_json  # => "{\"data\":\"sample\"}"

xml_record = DataRecord.new(:xml)
xml_record.to_xml   # => "<record><data>sample</data></record>"

Module prepending with prepend places modules before the class in the ancestor chain, allowing modules to override class methods while maintaining access to the original implementation through super.

module Loggable
  def process_data(data)
    puts "Starting processing: #{data}"
    result = super(data)
    puts "Finished processing: #{result}"
    result
  end
end

class DataProcessor
  prepend Loggable
  
  def process_data(data)
    data.upcase
  end
end

processor = DataProcessor.new
processor.process_data("hello")
# Starting processing: hello
# Finished processing: HELLO
# => "HELLO"

Extend can modify the behavior of individual objects within collection processing, applying different capabilities based on object properties or external conditions.

module Prioritizable
  def priority_score
    (@priority || 1) * 10
  end
end

module Urgent
  def priority_score
    super + 50
  end
end

tasks = [
  { name: "Deploy", priority: 3 },
  { name: "Review", priority: 1 },
  { name: "Hotfix", priority: 2, urgent: true }
]

processed_tasks = tasks.map do |task_data|
  task = Struct.new(:name, :priority).new(task_data[:name], task_data[:priority])
  task.extend(Prioritizable)
  task.extend(Urgent) if task_data[:urgent]
  task
end

processed_tasks.each { |task| puts "#{task.name}: #{task.priority_score}" }
# Deploy: 30
# Review: 10
# Hotfix: 70

Common Pitfalls

The most frequent confusion involves misunderstanding where extended methods become available. Extending an instance adds methods only to that specific object, not to the class or other instances.

module Helpers
  def utility_method
    "Available only where extended"
  end
end

class Worker
end

# Wrong expectation: this won't affect other Worker instances
worker1 = Worker.new
worker1.extend(Helpers)
worker1.utility_method  # => "Available only where extended"

worker2 = Worker.new
worker2.utility_method  # NoMethodError: undefined method `utility_method'

Class extension versus instance extension creates different method availability. Many developers expect Class.extend(Module) to make methods available on instances, but it makes them available as class methods.

module Calculatable
  def add(a, b)
    a + b
  end
end

# This makes add available as a class method, not instance method
class MathHelper
  extend Calculatable
end

MathHelper.add(2, 3)  # => 5
MathHelper.new.add(2, 3)  # NoMethodError

# To make add available on instances, use include
class MathHelper2
  include Calculatable
end

MathHelper2.new.add(2, 3)  # => 5
MathHelper2.add(2, 3)  # NoMethodError (no class method)

Module method lookup through super behaves differently with include, extend, and prepend. Understanding the ancestor chain helps predict which method super calls.

module A
  def test
    puts "A"
    super if defined?(super)
  end
end

module B
  def test
    puts "B"
    super if defined?(super)
  end
end

class Base
  def test
    puts "Base"
  end
end

class Example < Base
  include A
  include B
end

# B → A → Base
Example.new.test
# B
# A  
# Base

# But with extend on an instance:
example = Base.new
example.extend(A)
example.extend(B)
example.test
# B
# A
# Base (calls the original instance method)

Instance variable access differs between included and extended modules. Extended modules on classes cannot access instance variables of instances created from those classes.

module InstanceVarAccess
  def get_name
    @name
  end
  
  def set_name(name)
    @name = name
  end
end

class Person
  def initialize(name)
    @name = name
  end
end

# Include: works with instance variables
class Person1 < Person
  include InstanceVarAccess
end

person1 = Person1.new("Alice")
person1.get_name  # => "Alice"

# Extend on class: creates class-level instance variables
class Person2 < Person
  extend InstanceVarAccess
end

Person2.set_name("Bob")
Person2.get_name  # => "Bob" (class instance variable)

person2 = Person2.new("Charlie")
person2.get_name  # NoMethodError: undefined method `get_name'

Method visibility (private, protected) can produce unexpected results when modules are included versus extended. Private methods in extended modules become private singleton methods, which behave differently from regular private methods.

module SecretMethods
  private
  
  def secret
    "Secret information"
  end
  
  public
  
  def reveal_secret
    secret
  end
end

class Container
  include SecretMethods
end

container = Container.new
container.reveal_secret  # => "Secret information"
container.secret  # NoMethodError (private method)

# With extend:
other_container = Container.new
other_container.extend(SecretMethods)
other_container.reveal_secret  # => "Secret information"  
other_container.secret  # NoMethodError (private singleton method)

Reference

Core Methods

Method Parameters Returns Description
include module self Adds module methods as instance methods
extend module obj Adds module methods as singleton methods
prepend module self Adds module before class in ancestor chain
included_modules none Array Returns array of included modules
ancestors none Array Returns ancestor chain including modules

Module Callbacks

Callback Trigger Parameters Usage
included Module included in class/module base Hook for include operations
extended Module extends object object Hook for extend operations
prepended Module prepended to class/module base Hook for prepend operations

Method Lookup Order

Operation Lookup Path Example
Include Class → Modules (reverse order) → Superclass [Class, ModuleB, ModuleA, Superclass]
Extend Singleton class → Class → Modules → Superclass [#<Class:obj>, Class, Modules, Superclass]
Prepend Modules (reverse order) → Class → Superclass [ModuleB, ModuleA, Class, Superclass]

Visibility Rules

Method Type Include Behavior Extend Behavior
public Public instance method Public singleton method
private Private instance method Private singleton method
protected Protected instance method Protected singleton method

Common Patterns

Pattern Implementation Use Case
Mixin include Module Shared instance behavior
Class methods extend Module Utility functions, factories
Decorator object.extend(Module) Single object enhancement
Wrapper prepend Module Method interception

Decision Matrix

Goal Use Example
All instances need methods include Validation, persistence
Class needs utility methods extend Factories, builders
One object needs methods instance.extend Admin capabilities
Override with super access prepend Logging, monitoring
Chain multiple behaviors Multiple includes Authentication + logging

Introspection Methods

Method Returns Description
obj.singleton_methods Array<Symbol> Methods added via extend
Class.included_modules Array<Module> Modules included in class
obj.methods - Class.instance_methods Array<Symbol> Object-specific methods
Module.nesting Array<Module> Current module nesting context