Introduction
Ruby 2.0 introduced prepend as a new way to mix modules into classes. While include adds a module’s methods after the class in the method lookup chain, prepend inserts them before the class โ giving you the ability to wrap or override existing methods without monkey-patching.
Understanding the difference between prepend and include is key to writing clean, composable Ruby code.
The Method Lookup Chain
Ruby resolves method calls by walking an ordered list called the method lookup chain (or ancestor chain). You can inspect it with .ancestors:
class Animal; end
puts Animal.ancestors.inspect
# => [Animal, Object, Kernel, BasicObject]
When you include or prepend a module, it gets inserted into this chain at different positions.
include: Module Goes After the Class
With include, the module is inserted after the class in the ancestor chain:
module Greetable
def hello
"Hello from Greetable"
end
end
class Person
include Greetable
def hello
"Hello from Person"
end
end
puts Person.ancestors.inspect
# => [Person, Greetable, Object, Kernel, BasicObject]
puts Person.new.hello
# => "Hello from Person" (Person's method wins)
Because Person comes before Greetable in the chain, Person#hello takes precedence.
prepend: Module Goes Before the Class
With prepend, the module is inserted before the class:
module Greetable
def hello
"Hello from Greetable"
end
end
class Person
prepend Greetable
def hello
"Hello from Person"
end
end
puts Person.ancestors.inspect
# => [Greetable, Person, Object, Kernel, BasicObject]
puts Person.new.hello
# => "Hello from Greetable" (Greetable's method wins)
Now Greetable#hello is called first. This is the key difference.
The Power of prepend: Wrapping Methods
The most useful pattern with prepend is wrapping an existing method โ calling super to invoke the original while adding behavior around it:
module Logging
def save
puts "[LOG] Before save"
result = super # calls the original Person#save
puts "[LOG] After save"
result
end
end
class Person
prepend Logging
def save
puts "Saving person..."
true
end
end
Person.new.save
# => [LOG] Before save
# => Saving person...
# => [LOG] After save
This is a clean alternative to alias-based monkey-patching.
Practical Use Cases
1. Adding Instrumentation / Metrics
module Instrumented
def process(data)
start = Time.now
result = super
elapsed = Time.now - start
puts "process() took #{elapsed.round(4)}s"
result
end
end
class DataPipeline
prepend Instrumented
def process(data)
# heavy processing...
data.upcase
end
end
DataPipeline.new.process("hello")
# => process() took 0.0001s
2. Input Validation
module Validated
def create_user(name, age)
raise ArgumentError, "Name can't be blank" if name.to_s.strip.empty?
raise ArgumentError, "Age must be positive" unless age.to_i > 0
super
end
end
class UserService
prepend Validated
def create_user(name, age)
puts "Creating user: #{name}, age #{age}"
end
end
UserService.new.create_user("Alice", 30) # => Creating user: Alice, age 30
UserService.new.create_user("", 30) # => ArgumentError: Name can't be blank
3. Caching
module Memoizable
def expensive_calculation(n)
@cache ||= {}
@cache[n] ||= super
end
end
class MathService
prepend Memoizable
def expensive_calculation(n)
sleep(0.1) # simulate heavy work
n * n
end
end
svc = MathService.new
puts svc.expensive_calculation(5) # slow first time
puts svc.expensive_calculation(5) # instant from cache
include vs prepend: Side-by-Side Comparison
module M
def who
"M -> #{super rescue 'end'}"
end
end
class WithInclude
include M
def who; "WithInclude"; end
end
class WithPrepend
prepend M
def who; "WithPrepend"; end
end
puts WithInclude.new.who # => "WithInclude" (class wins)
puts WithPrepend.new.who # => "M -> WithPrepend" (module wraps class)
puts WithInclude.ancestors.first(3).inspect
# => [WithInclude, M, Object]
puts WithPrepend.ancestors.first(3).inspect
# => [M, WithPrepend, Object]
When to Use Each
| Scenario | Use |
|---|---|
| Adding new methods to a class | include |
| Sharing utility methods across classes | include |
| Wrapping / decorating existing methods | prepend |
| Adding before/after hooks without alias | prepend |
| Instrumentation, logging, caching layers | prepend |
Multiple prepend Calls
You can prepend multiple modules. They stack in reverse order of declaration:
module A
def greet; "A -> #{super}"; end
end
module B
def greet; "B -> #{super}"; end
end
class Base
prepend A, B
def greet; "Base"; end
end
puts Base.ancestors.first(4).inspect
# => [A, B, Base, Object]
puts Base.new.greet
# => "A -> B -> Base"
Common Pitfalls
Forgetting super
If you prepend a module to wrap a method but forget super, the original method never runs:
module BadWrapper
def save
puts "Before save"
# forgot super โ original save never called!
end
end
Prepending to the Wrong Level
prepend on a module affects instances. To affect class-level methods, use prepend inside class << self:
module ClassLogging
def find(id)
puts "Finding #{id}..."
super
end
end
class User
class << self
prepend ClassLogging
end
def self.find(id)
"User ##{id}"
end
end
puts User.find(42)
# => Finding 42...
# => User #42
Comments