Overview
The prepend
method inserts a module into the method lookup chain before the including class or module, creating a mechanism for method interception. Unlike include
, which places modules after the current class in the lookup chain, prepend
places modules before the class, allowing the module's methods to intercept calls to methods with the same name in the class.
Ruby's method lookup chain determines which method implementation executes when a method is called. With prepend
, the module's methods execute first, and can call super
to invoke the original class method. This creates a wrapper pattern where the prepended module acts as middleware around the original method implementation.
The primary use cases for prepend
include logging method calls, performance monitoring, input validation, caching, authentication, and adding cross-cutting concerns without modifying the original class definition.
module Logging
def save
puts "Before save"
result = super
puts "After save: #{result}"
result
end
end
class User
prepend Logging
def save
puts "Saving user"
true
end
end
User.new.save
# Before save
# Saving user
# After save: true
# => true
The ancestors
method reveals the method lookup order. After prepending, the module appears before the class in the ancestor chain:
User.ancestors
# => [Logging, User, Object, Kernel, BasicObject]
This ordering means when save
is called on a User
instance, Ruby first checks Logging#save
, which then calls super
to invoke User#save
.
Basic Usage
Method interception with prepend
follows a standard pattern: define methods in a module with the same names as the target class methods, perform pre-processing, call super
to execute the original method, perform post-processing, and return the result.
module Validator
def create(attributes)
raise ArgumentError, "Invalid attributes" if attributes.empty?
super(attributes.transform_keys(&:to_s))
end
end
class Product
prepend Validator
def create(attributes)
@attributes = attributes
"Product created with #{@attributes}"
end
end
product = Product.new
product.create(name: "Widget", price: 10.99)
# => "Product created with {\"name\"=>\"Widget\", \"price\"=>10.99}"
product.create({})
# ArgumentError: Invalid attributes
Multiple modules can be prepended to the same class. Ruby processes them in reverse order of prepending, with the last prepended module appearing first in the ancestor chain:
module Timer
def process
start_time = Time.now
result = super
end_time = Time.now
puts "Execution time: #{end_time - start_time} seconds"
result
end
end
module Logger
def process
puts "Starting process"
result = super
puts "Process completed"
result
end
end
class DataProcessor
prepend Timer
prepend Logger # This will be called first
def process
sleep(0.1)
"Data processed"
end
end
DataProcessor.new.process
# Starting process
# Data processed
# Process completed
# Execution time: 0.100123 seconds
# => "Data processed"
The ancestor chain shows the order:
DataProcessor.ancestors[0..2]
# => [Logger, Timer, DataProcessor]
Prepending works with inheritance hierarchies. The prepended module affects only the class it's prepended to, not parent or child classes:
module Auditing
def delete
puts "Audit: #{self.class} delete called"
super
end
end
class BaseModel
def delete
"Base delete"
end
end
class User < BaseModel
prepend Auditing
def delete
"User delete"
end
end
class Admin < User
def delete
"Admin delete"
end
end
User.new.delete
# Audit: User delete called
# => "User delete"
Admin.new.delete
# => "Admin delete" # No audit logging
Advanced Usage
Complex interception patterns emerge when combining multiple modules, conditional logic, and metaprogramming techniques. Stacking interceptors creates layered middleware where each module adds specific functionality:
module Caching
def find(id)
cache_key = "#{self.class.name.downcase}_#{id}"
if @cache && @cache[cache_key]
puts "Cache hit for #{cache_key}"
return @cache[cache_key]
end
result = super
@cache ||= {}
@cache[cache_key] = result
puts "Cache miss for #{cache_key}, cached result"
result
end
end
module RateLimiting
def find(id)
@request_count ||= 0
@request_count += 1
if @request_count > 5
raise "Rate limit exceeded"
end
puts "Rate limit check passed (#{@request_count}/5)"
super
end
end
module DatabaseMetrics
def find(id)
start_time = Time.now
result = super
query_time = Time.now - start_time
@metrics ||= []
@metrics << { query_time: query_time, id: id }
puts "Query completed in #{query_time.round(4)}s"
result
end
end
class UserRepository
prepend Caching
prepend RateLimiting
prepend DatabaseMetrics
def find(id)
sleep(0.02) # Simulate database query
{ id: id, name: "User #{id}", email: "user#{id}@example.com" }
end
end
repo = UserRepository.new
user = repo.find(1)
# Rate limit check passed (1/5)
# Query completed in 0.0201s
# Cache miss for userrepository_1, cached result
user = repo.find(1)
# Rate limit check passed (2/5)
# Cache hit for userrepository_1
Conditional interception allows modules to selectively intercept methods based on runtime conditions:
module ConditionalLogging
def self.prepended(base)
base.extend(ClassMethods)
end
module ClassMethods
def logged_methods(*methods)
@logged_methods = methods
end
def should_log?(method_name)
@logged_methods&.include?(method_name)
end
end
def method_missing(method_name, *args, **kwargs, &block)
if self.class.should_log?(method_name)
puts "Calling #{method_name} with args: #{args.inspect}"
result = super
puts "#{method_name} returned: #{result.inspect}"
result
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
self.class.should_log?(method_name) || super
end
end
class ApiClient
prepend ConditionalLogging
logged_methods :fetch_user, :create_user
def fetch_user(id)
{ id: id, name: "User #{id}" }
end
def create_user(data)
{ id: rand(1000), **data }
end
def internal_method
"internal operation"
end
end
client = ApiClient.new
client.fetch_user(123)
# Calling fetch_user with args: [123]
# fetch_user returned: {:id=>123, :name=>"User 123"}
client.internal_method
# => "internal operation" # No logging
Dynamic module generation creates interceptors programmatically based on configuration:
module InterceptorFactory
def self.create_validator(rules)
Module.new do
rules.each do |method_name, validations|
define_method(method_name) do |*args, **kwargs|
validations.each do |validation|
case validation[:type]
when :presence
param_index = validation[:param]
raise ArgumentError, "#{validation[:param]} cannot be nil" if args[param_index].nil?
when :type
param_index = validation[:param]
expected_type = validation[:class]
unless args[param_index].is_a?(expected_type)
raise TypeError, "Expected #{expected_type}, got #{args[param_index].class}"
end
end
end
super(*args, **kwargs)
end
end
end
end
end
validation_rules = {
create: [
{ type: :presence, param: 0 },
{ type: :type, param: 0, class: Hash }
],
update: [
{ type: :presence, param: 0 },
{ type: :presence, param: 1 },
{ type: :type, param: 1, class: Hash }
]
}
validator = InterceptorFactory.create_validator(validation_rules)
class UserService
prepend validator
def create(data)
"Created user with #{data}"
end
def update(id, data)
"Updated user #{id} with #{data}"
end
end
service = UserService.new
service.create({ name: "John" })
# => "Created user with {\"name\"=>\"John\"}"
service.create(nil)
# ArgumentError: 0 cannot be nil
Common Pitfalls
The super
keyword behavior in prepended modules differs from inheritance. In prepended modules, super
calls the next method in the lookup chain, which may be the original class method or another prepended module. Omitting super
breaks the chain and prevents the original method from executing:
module BrokenInterceptor
def process
puts "Before processing"
# Missing super call - original method never executes
"intercepted result"
end
end
module WorkingInterceptor
def process
puts "Before processing"
result = super
puts "After processing"
result
end
end
class Worker
def process
"original result"
end
end
class BrokenWorker < Worker
prepend BrokenInterceptor
end
class WorkingWorker < Worker
prepend WorkingInterceptor
end
BrokenWorker.new.process
# Before processing
# => "intercepted result" # Original method never called
WorkingWorker.new.process
# Before processing
# original result
# After processing
# => "original result"
Method signature mismatches between the intercepting module and the target class cause argument errors. The intercepting method must accept the same parameters or use flexible parameter handling:
module IncorrectSignature
def calculate(x) # Original method takes x, y
puts "Calculating with #{x}"
super
end
end
module FlexibleSignature
def calculate(*args, **kwargs)
puts "Calculating with args: #{args}, kwargs: #{kwargs}"
super
end
end
class Calculator
def calculate(x, y, operation: :add)
case operation
when :add then x + y
when :multiply then x * y
end
end
end
class BrokenCalculator < Calculator
prepend IncorrectSignature
end
class FlexibleCalculator < Calculator
prepend FlexibleSignature
end
# BrokenCalculator.new.calculate(5, 3)
# ArgumentError: wrong number of arguments (given 2, expected 1)
FlexibleCalculator.new.calculate(5, 3, operation: :multiply)
# Calculating with args: [5, 3], kwargs: {:operation=>:multiply}
# => 15
Module prepending order creates unexpected behavior when multiple modules define the same method. The last prepended module appears first in the method lookup chain:
module FirstModule
def greet
puts "First module"
super
end
end
module SecondModule
def greet
puts "Second module"
super
end
end
class Greeter
prepend FirstModule
prepend SecondModule # This will be called first
def greet
puts "Original method"
end
end
Greeter.new.greet
# Second module
# First module
# Original method
Greeter.ancestors[0..2]
# => [SecondModule, FirstModule, Greeter]
Prepending modules that don't call super
creates silent method replacement instead of interception:
module ReplacementModule
def important_method
"replaced implementation"
# No super call - original method is completely replaced
end
end
module InterceptionModule
def important_method
puts "Intercepting call"
result = super
puts "Original returned: #{result}"
result
end
end
class ImportantClass
def important_method
puts "Critical business logic"
"original result"
end
end
class ReplacedClass < ImportantClass
prepend ReplacementModule
end
class InterceptedClass < ImportantClass
prepend InterceptionModule
end
ReplacedClass.new.important_method
# => "replaced implementation" # Critical logic never runs
InterceptedClass.new.important_method
# Intercepting call
# Critical business logic
# Original returned: original result
# => "original result"
Infinite recursion occurs when prepended modules call methods that trigger the same interception:
module RecursiveModule
def process_data(data)
if data.is_a?(String)
# This calls process_data again, causing infinite recursion
process_data(data.split(","))
else
super
end
end
end
class DataProcessor
prepend RecursiveModule
def process_data(data)
"Processed: #{data.inspect}"
end
end
# DataProcessor.new.process_data("a,b,c")
# SystemStackError: stack level too deep
The correct approach uses different method names or direct class method calls:
module SafeModule
def process_data(data)
if data.is_a?(String)
# Call a different method or the class method directly
super(data.split(","))
else
super
end
end
end
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
prepend(module) |
module (Module) |
self |
Prepends module to the current class or module |
prepended(base) |
base (Class/Module) |
nil |
Hook called when module is prepended to base |
ancestors |
None | Array<Class/Module> |
Returns method lookup chain with prepended modules first |
Module Definition Patterns
Pattern | Syntax | Use Case |
---|---|---|
Simple Interception | def method_name; super; end |
Basic method wrapping |
Pre/Post Processing | pre_logic; result = super; post_logic; result |
Adding behavior before/after |
Conditional Interception | condition ? super : alternate_logic |
Selective method execution |
Parameter Transformation | super(transform(args)) |
Modifying method arguments |
Prepending Hooks
module HookedModule
def self.prepended(base)
# Called when module is prepended
# base is the class/module that prepended this module
base.extend(ClassMethods) if defined?(ClassMethods)
end
end
Method Lookup Chain Behavior
Scenario | Ancestor Order | Method Resolution |
---|---|---|
No prepending | [Class, Parent, Object] |
Standard inheritance |
Single prepend | [Module, Class, Parent] |
Module method first |
Multiple prepends | [Last, Previous, ..., Class] |
Reverse prepend order |
Inheritance with prepend | [Module, Child, Parent] |
Module only affects target class |
Super Call Variations
Super Syntax | Behavior | Arguments Passed |
---|---|---|
super |
Calls next method with same arguments | Original arguments |
super() |
Calls next method with no arguments | None |
super(args) |
Calls next method with specified arguments | Specified arguments |
super(*args, **kwargs) |
Calls next method with splat arguments | Splat arguments |
Common Method Signature Patterns
# Exact signature matching
def method_name(param1, param2, keyword: nil)
super
end
# Flexible parameter handling
def method_name(*args, **kwargs, &block)
super
end
# Hybrid approach
def method_name(required_param, *args, **kwargs)
super
end
Error Types and Causes
Error | Cause | Solution |
---|---|---|
ArgumentError |
Parameter count mismatch | Use flexible signatures or match exactly |
SystemStackError |
Infinite recursion in interceptor | Avoid calling same method in module |
NoMethodError |
Missing super or wrong method name |
Ensure method exists and super is called |
TypeError |
Incorrect return type transformation | Preserve or correctly transform return values |
Module Ordering Debugging
# Check ancestor chain
ClassName.ancestors
# Find method source
ClassName.instance_method(:method_name).source_location
# List all method definitions
ClassName.ancestors.map { |a| a.instance_methods(false) if a.method_defined?(:method_name) }
Performance Considerations
- Each prepended module adds a step in method lookup
- Multiple interceptors create nested call stacks
- Conditional logic in interceptors affects all method calls
- Method resolution traverses ancestor chain in order