Overview
Ruby provides two module callback methods, included
and extended
, that execute automatically when a module gets mixed into a class or object. The included
method triggers when a module is included via include
, while extended
triggers when a module is mixed in via extend
. These callbacks receive the target class or object as an argument, creating opportunities for dynamic behavior modification and metaprogramming.
When Ruby encounters include ModuleName
in a class definition, it first adds the module's instance methods to the class, then calls ModuleName.included(TargetClass)
if the method exists. Similarly, extend ModuleName
adds the module's methods as singleton methods to the target, then calls ModuleName.extended(TargetObject)
.
module Trackable
def self.included(base)
puts "#{self} included in #{base}"
end
def track_action
puts "Action tracked"
end
end
class User
include Trackable
end
# Output: Trackable included in User
The callback methods execute in the context of the module itself, not the target class. This distinction affects which variables and methods are accessible within the callback. The base
parameter represents the class or object receiving the module's functionality.
module Configurable
@default_config = { timeout: 30 }
def self.extended(obj)
obj.instance_variable_set(:@config, @default_config.dup)
end
def configure(options = {})
@config.merge!(options)
end
end
api_client = Object.new
api_client.extend(Configurable)
# Configurable.extended(api_client) called automatically
These callbacks form the foundation for many Ruby design patterns including class-level DSLs, automatic method generation, and registration systems. The timing of execution makes them particularly useful for modifying the target class's structure or establishing initial state.
Basic Usage
Module callbacks typically modify the target class by adding class methods, defining constants, or establishing instance variable defaults. The most common pattern involves adding both instance and class methods from a single module inclusion.
module Timestampable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
attr_accessor :created_at, :updated_at
end
end
module ClassMethods
def with_timestamps
all.select { |obj| obj.created_at && obj.updated_at }
end
end
def touch
@updated_at = Time.now
end
end
class Document
include Timestampable
def initialize
@created_at = Time.now
end
end
doc = Document.new
doc.touch
Document.with_timestamps # Class method available
The extended
callback works differently since extend
adds methods as singleton methods rather than instance methods. This makes it ideal for adding functionality to specific objects or creating module-based configuration systems.
module Cacheable
def self.extended(obj)
obj.instance_variable_set(:@cache, {})
def obj.cache_key_for(method_name)
"#{self.class.name}##{method_name}"
end
end
def cached_method(name, &block)
key = cache_key_for(name)
@cache[key] ||= instance_eval(&block)
end
end
class ExpensiveCalculator
extend Cacheable
def fibonacci(n)
cached_method("fibonacci_#{n}") do
n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2)
end
end
end
Registration patterns commonly use these callbacks to maintain lists of classes that include specific modules. This approach enables automatic discovery and processing of related classes.
module Serializable
@serializable_classes = []
def self.included(base)
@serializable_classes << base
base.extend(ClassMethods)
end
def self.all_serializable_classes
@serializable_classes
end
module ClassMethods
def serializable_fields(*fields)
@serializable_fields = fields
end
def get_serializable_fields
@serializable_fields || []
end
end
def serialize
fields = self.class.get_serializable_fields
fields.each_with_object({}) do |field, hash|
hash[field] = send(field)
end
end
end
class User
include Serializable
serializable_fields :name, :email
attr_accessor :name, :email, :password
end
class Product
include Serializable
serializable_fields :title, :price
attr_accessor :title, :price, :internal_code
end
Serializable.all_serializable_classes
# => [User, Product]
Both callbacks can access and modify class-level instance variables, making them suitable for maintaining module-specific state across multiple classes. The key difference lies in how the methods become available: include
creates instance methods while extend
creates singleton methods on the target.
Advanced Usage
Module callbacks enable sophisticated metaprogramming patterns through dynamic method generation and class structure modification. These techniques often involve creating domain-specific languages or implementing complex inheritance hierarchies.
module Validatable
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@validations, {})
base.instance_variable_set(:@validation_errors, [])
end
module ClassMethods
def validates(field, **options)
@validations[field] = options
# Generate custom validation methods
options.each do |validator, constraint|
method_name = "validate_#{field}_#{validator}"
define_validation_method(method_name, field, validator, constraint)
end
end
def validation_errors
@validation_errors
end
private
def define_validation_method(method_name, field, validator, constraint)
case validator
when :presence
define_method(method_name) do
value = send(field)
if value.nil? || (value.respond_to?(:empty?) && value.empty?)
self.class.instance_variable_get(:@validation_errors) << "#{field} cannot be blank"
false
else
true
end
end
when :length
define_method(method_name) do
value = send(field)
return true if value.nil?
min, max = constraint.values_at(:min, :max)
length = value.length
if min && length < min
self.class.instance_variable_get(:@validation_errors) << "#{field} is too short"
false
elsif max && length > max
self.class.instance_variable_get(:@validation_errors) << "#{field} is too long"
false
else
true
end
end
end
end
end
def validate
self.class.instance_variable_get(:@validations).each do |field, options|
options.each do |validator, _|
method_name = "validate_#{field}_#{validator}"
send(method_name)
end
end
self.class.validation_errors.empty?
end
end
class Account
include Validatable
validates :username, presence: true, length: { min: 3, max: 20 }
validates :email, presence: true
attr_accessor :username, :email
end
account = Account.new
account.validate # false
Account.validation_errors
# => ["username cannot be blank", "email cannot be blank"]
Callback methods can implement complex delegation patterns by dynamically creating methods that forward calls to composed objects. This technique supports building flexible architectures without traditional inheritance.
module Delegatable
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@delegated_methods, {})
end
module ClassMethods
def delegate(*methods, to:, prefix: nil, allow_nil: false)
target_methods = methods.flatten
@delegated_methods[to] = target_methods
target_methods.each do |method|
delegated_method_name = prefix ? "#{prefix}_#{method}" : method
define_method(delegated_method_name) do
target_object = send(to)
if target_object.nil?
if allow_nil
nil
else
raise "#{to} is nil, cannot delegate #{method}"
end
else
target_object.send(method)
end
end
end
end
def delegated_methods
@delegated_methods
end
end
end
class Order
include Delegatable
delegate :name, :email, to: :customer
delegate :total, :currency, to: :payment, prefix: :payment
delegate :street, :city, to: :shipping_address, allow_nil: true
attr_accessor :customer, :payment, :shipping_address
end
class Customer
attr_accessor :name, :email
end
class Payment
attr_accessor :total, :currency
end
order = Order.new
customer = Customer.new
customer.name = "John Doe"
customer.email = "john@example.com"
order.customer = customer
order.name # "John Doe" - delegated to customer.name
order.email # "john@example.com" - delegated to customer.email
Module callbacks support building plugin architectures through automatic hook registration and method interception. This pattern enables extending application behavior without modifying core classes.
module Pluggable
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@hooks, Hash.new { |h, k| h[k] = [] })
base.instance_variable_set(:@plugins, [])
end
module ClassMethods
def install_plugin(plugin_module)
include plugin_module
@plugins << plugin_module
# Register plugin hooks
plugin_module.constants.each do |const_name|
const_value = plugin_module.const_get(const_name)
if const_value.respond_to?(:call) && const_name.to_s.end_with?('_HOOK')
hook_name = const_name.to_s.sub('_HOOK', '').downcase.to_sym
@hooks[hook_name] << const_value
end
end
end
def trigger_hooks(hook_name, *args)
@hooks[hook_name].each { |hook| hook.call(*args) }
end
def installed_plugins
@plugins
end
end
def execute_with_hooks(method_name, *args)
self.class.trigger_hooks(:before, method_name, *args)
result = send("#{method_name}_without_hooks", *args)
self.class.trigger_hooks(:after, method_name, result, *args)
result
end
end
module LoggingPlugin
BEFORE_HOOK = lambda do |method_name, *args|
puts "Calling #{method_name} with #{args.inspect}"
end
AFTER_HOOK = lambda do |method_name, result, *args|
puts "#{method_name} returned #{result.inspect}"
end
end
class Calculator
include Pluggable
install_plugin LoggingPlugin
def add(a, b)
execute_with_hooks(:add, a, b)
end
def add_without_hooks(a, b)
a + b
end
end
calc = Calculator.new
calc.add(5, 3)
# Output:
# Calling add with [5, 3]
# add returned 8
These advanced patterns demonstrate how module callbacks can transform simple inclusion mechanisms into powerful architectural tools for building maintainable and extensible applications.
Common Pitfalls
Module callbacks execute during class definition time, not instance creation time, which creates timing-related issues that catch developers off guard. Code that depends on instance state or runtime configuration may behave unexpectedly.
module Problematic
def self.included(base)
# This runs during class definition, not instantiation
base.class_eval do
def current_time
Time.now # Always returns the time when class was defined
end
end
end
end
class TimestampedClass
include Problematic
end
# Multiple instances return the same time
obj1 = TimestampedClass.new
sleep(2)
obj2 = TimestampedClass.new
obj1.current_time == obj2.current_time # true - both return class definition time
The correct approach defers execution until runtime by defining methods that calculate values when called rather than when defined.
module CorrectTiming
def self.included(base)
base.class_eval do
define_method(:current_time) do
Time.now # Calculated each time method is called
end
end
end
end
Variable scope within callbacks often confuses developers because the callback executes in the module's context, not the target class context. This affects access to instance variables and local variables.
module ScopeConfusion
@module_variable = "module scope"
def self.included(base)
# @module_variable refers to module's instance variable
puts @module_variable # "module scope"
# Trying to access base's instance variables requires explicit reference
base.instance_variable_set(:@class_variable, "class scope")
# Local variables from surrounding scope are not accessible
local_var = "callback scope"
base.define_method(:show_variables) do
# local_var is not accessible here
# @module_variable is not accessible here unless explicitly set
@class_variable # This works, was set by callback
end
end
end
Method naming conflicts occur when multiple modules define the same callback methods or when callbacks define methods that clash with existing methods. The last module included wins, potentially overriding previous behavior silently.
module FirstModule
def self.included(base)
base.define_method(:process) do
"First module processing"
end
end
end
module SecondModule
def self.included(base)
base.define_method(:process) do
"Second module processing" # Overwrites FirstModule's process method
end
end
end
class Processor
include FirstModule
include SecondModule # SecondModule's process method wins
end
Processor.new.process # "Second module processing"
Inheritance hierarchies create unexpected callback execution order when modules are included at different class levels. Callbacks execute for each inclusion, but method resolution follows Ruby's standard lookup path.
module TrackingModule
def self.included(base)
puts "Tracking included in #{base}"
base.extend(ClassMethods)
end
module ClassMethods
def tracked_method
"tracking from #{self}"
end
end
end
class BaseClass
include TrackingModule # Callback executes
end
class SubClass < BaseClass
include TrackingModule # Callback executes again, methods redefined
end
# Output:
# Tracking included in BaseClass
# Tracking included in SubClass
SubClass.tracked_method # "tracking from SubClass"
BaseClass.tracked_method # "tracking from BaseClass"
Class-level instance variable sharing between modules and target classes creates hidden dependencies that break when module internal implementation changes. Modules should not assume target classes maintain specific instance variable structures.
module ProblematicSharing
def self.included(base)
# Assumes base class will maintain @items array
base.instance_variable_set(:@items, [])
base.define_method(:add_item) do |item|
# Directly manipulates class instance variable
self.class.instance_variable_get(:@items) << item
end
end
end
class Container
include ProblematicSharing
# Later refactoring changes @items to @elements
def initialize
@elements = [] # Module still expects @items
end
end
container = Container.new
container.add_item("test") # Creates separate @items array, @elements remains empty
The solution involves establishing clear contracts between modules and target classes, or using accessor methods instead of direct instance variable manipulation.
module BetterSharing
def self.included(base)
base.class_eval do
attr_accessor :items
def initialize(*args)
super(*args)
@items = [] unless defined?(@items)
end
end
end
def add_item(item)
self.items << item # Uses accessor method
end
end
Reference
Module Callback Methods
Method | Trigger | Context | Parameters | Purpose |
---|---|---|---|---|
self.included(base) |
include ModuleName |
Module | Target class | Execute code when module included |
self.extended(object) |
extend ModuleName |
Module | Target object | Execute code when module extended |
self.prepended(base) |
prepend ModuleName |
Module | Target class | Execute code when module prepended |
Callback Execution Context
Aspect | Details |
---|---|
Execution Time | Class definition time, not instance creation |
Receiver Context | Module itself (self refers to module) |
Variable Scope | Module's scope, not target class scope |
Method Definitions | Use base.define_method or base.class_eval |
Instance Variables | Module's instance variables accessible via @var |
Common Callback Patterns
Pattern | Implementation | Use Case |
---|---|---|
Add Class Methods | base.extend(ClassMethods) |
Add class-level functionality |
Define Instance Methods | base.define_method(name, &block) |
Dynamic method creation |
Set Instance Variables | base.instance_variable_set(:@var, value) |
Initialize class state |
Class Evaluation | base.class_eval { code } |
Execute code in class context |
Registration | @@registry << base |
Track classes using module |
Method Definition Techniques
Technique | Syntax | When to Use |
---|---|---|
define_method | base.define_method(:name) { code } |
Dynamic method names or closures |
class_eval string | base.class_eval("def name; code; end") |
String-based method definitions |
class_eval block | base.class_eval { def name; code; end } |
Block-based method definitions |
module_eval | base.module_eval { code } |
Module context evaluation |
Execution Order
Scenario | Order |
---|---|
Single Include | Include module methods → Call included |
Multiple Includes | Process in order of inclusion |
Inheritance | Callbacks execute for each class separately |
Nested Modules | Inner modules first, outer modules second |
Common Method Patterns
# Class method addition pattern
def self.included(base)
base.extend(ClassMethods)
end
# Instance variable initialization pattern
def self.included(base)
base.instance_variable_set(:@collection, [])
end
# Dynamic method generation pattern
def self.included(base)
base.define_method(:dynamic_method) do |arg|
"Generated method with #{arg}"
end
end
# Registration pattern
def self.included(base)
registered_classes << base
end
# Configuration pattern
def self.extended(obj)
obj.instance_variable_set(:@config, default_config)
end
Error Conditions
Error | Cause | Solution |
---|---|---|
NoMethodError | Callback defines methods incorrectly | Use proper method definition techniques |
NameError | Variable scope confusion | Explicitly reference target class variables |
ArgumentError | Callback expects different parameters | Check callback method signature |
RuntimeError | Timing issues with instance state | Defer execution to method call time |
Debugging Techniques
Issue | Debugging Approach |
---|---|
Callback Not Executing | Verify callback method name and definition |
Wrong Method Context | Check if using define_method vs class_eval |
Variable Access Problems | Print available instance variables in callback |
Method Override Issues | Check module inclusion order |
Inheritance Conflicts | Trace method lookup chain with ancestors |
Performance Considerations
Consideration | Impact | Mitigation |
---|---|---|
Callback Complexity | Slows class definition | Keep callbacks simple |
Method Generation | Memory usage for many methods | Generate methods conditionally |
Class Variable Access | Slower than instance variables | Cache frequently accessed values |
Deep Inheritance | Multiple callback executions | Design shallow hierarchies |