Extending code with prepend
This chapter presents techniques for extending existing code without modifying it directly. This is a fundamental concept in Ruby: you can enrich a class after its creation, even if you do not have access to the source file.
Principle
Imagine we are using a Pokemon class written by someone else. We want to add a logging system that displays a message every time a Pokemon takes damage. But we do not want to (and sometimes cannot) modify the original file.
Ruby offers three solutions:
- Reopening classes: we reopen the class to add or modify methods. Simple but risky if we overwrite an existing method.
- alias: the old technique for saving a method before redefining it. Fragile.
- prepend: the modern and safe technique. We insert a module before the class in the ancestor chain.
Reopening classes
In Ruby, classes are never "closed". You can reopen them to add methods:
class Pokemon
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
def to_s
return "#{@name} Lvl.#{@level}"
end
end
# Later in the code (or in another file)
class Pokemon
def can_evolve?
return @level >= 16
end
end
pikachu = Pokemon.new('Pikachu', 25)
puts pikachu.can_evolve? # => true
puts pikachu # => Pikachu Lvl.25 (to_s still works)
- Adding a method by reopening the class is safe. The original is not touched.
- Redefining an existing method is risky: the original behavior is lost.
alias — the old technique (to avoid)
alias saves a method under another name before redefining it:
class Pokemon
alias original_to_s to_s
def to_s
return "[Modified] #{original_to_s}"
end
end
puts Pokemon.new('Pikachu', 25) # => [Modified] Pikachu Lvl.25
alias original_to_s to_screates a copy ofto_sunder the nameoriginal_to_s.- The problem: if two plugins use
aliason the same method, the second overwrites the first. This is fragile and a source of bugs that are hard to diagnose. aliasis a historical technique. It is no longer used in modern Ruby.
prepend — the modern technique
prepend inserts a module before the class in the ancestor chain. The module can call super to execute the original method:
class Pokemon
attr_reader :name, :hp
def initialize(name, hp)
@name = name
@hp = hp
end
def take_damage(amount)
@hp = (@hp - amount).clamp(0, 999)
return nil
end
end
# The module that extends the behavior
module DamageLog
def take_damage(amount)
puts "#{name} takes #{amount} damage!"
super
puts "#{name} has #{hp} HP remaining."
end
end
# We insert the module BEFORE the class
Pokemon.prepend(DamageLog)
pikachu = Pokemon.new('Pikachu', 100)
pikachu.take_damage(30)
Output:
Pikachu takes 30 damage!
Pikachu has 70 HP remaining.
superin the module calls the method from the original class. The original behavior is preserved and enriched.- The module can execute code before and after
super. - Unlike
alias, multiple modules can be prepended without conflict. Eachsupercalls the next one in the chain.
The ancestor chain with prepend
prepend modifies the order in .ancestors:
puts Pokemon.ancestors.inspect
# Before: [Pokemon, Object, Kernel, BasicObject]
Pokemon.prepend(DamageLog)
puts Pokemon.ancestors.inspect
# After: [DamageLog, Pokemon, Object, Kernel, BasicObject]
- The module
DamageLogis now at the head of the chain, beforePokemon. - When we call
pikachu.take_damage(30), Ruby looks for the method in order: first inDamageLog, then inPokemon. superinDamageLogpasses to the next element in the chain:Pokemon.
Multiple prepends
You can prepend multiple modules. Each super goes up the chain:
module DamageLog
def take_damage(amount)
puts "[LOG] #{name} takes #{amount} damage"
super
end
end
module MagicShield
def take_damage(amount)
reduced_damage = (amount * 0.8).to_i
puts "[SHIELD] Damage reduced from #{amount} to #{reduced_damage}"
super(reduced_damage)
end
end
Pokemon.prepend(DamageLog)
Pokemon.prepend(MagicShield)
puts Pokemon.ancestors.inspect
# => [MagicShield, DamageLog, Pokemon, Object, ...]
- The last prepended module is at the head. The call order is:
MagicShield->DamageLog->Pokemon. super(reduced_damage)inMagicShieldpasses the reduced damage toDamageLog, which callssupertowardPokemon.- This is what makes
prependso powerful: each plugin stacks without breaking the others.
include vs prepend vs extend
Summary of the three ways to inject a module:
class Pokemon
include Displayable # After the class (utilities)
prepend DamageLog # Before the class (interception)
extend Search # Class methods
end
puts Pokemon.ancestors.inspect
# => [DamageLog, Pokemon, Displayable, Object, ...]
| Method | Position in ancestors | Called via | Usage |
|---|---|---|---|
include | After the class | Instance (pikachu.fiche) | Adding utilities |
prepend | Before the class | Instance (pikachu.take_damage) | Intercepting & enriching |
extend | Outside the chain | Class (Pokemon.search(...)) | Class methods |
include: the module is inserted after the class. The class takes priority. Used for behavior sharing (Enumerable, Comparable).prepend: the module is inserted before the class. The module takes priority. Used for interception and extension.extend: the methods become class methods. Not in the instances'.ancestors.
Important rule
Never replace an existing class with another hierarchy. Always use prepend to add behavior:
# Do NOT do this
class Pokemon
def take_damage(amount)
# complete rewrite, the original behavior is lost
end
end
# Do this instead
module MyExtension
def take_damage(amount)
# added code
super # the original behavior is preserved
end
end
Pokemon.prepend(MyExtension)
Conclusion
- Ruby classes are open. Adding methods by reopening is safe. Overwriting existing methods loses the original behavior.
aliasis the old technique, fragile and obsolete. Do not use it anymore.prependinserts a module before the class in.ancestors.supercalls the original method.- Multiple prepended modules stack without conflict. Each
superpasses to the next one. includeadds after the class (utilities).prependadds before (interception).extendadds class methods.- Always use
prependwithsuperto extend existing code.