Skip to main content
View
Chapter 11 / 16Extending code with prepend
1. Variables and data types
2. Strings and symbols
3. Arrays
4. Hashes
5. Conditionals
6. Loops and iterators
7. Methods and blocks
8. Classes and objects
9. Inheritance
10. Modules
11. Extending code with prepend
12. Enumerable
13. Where to go next
14. Error handling
15. Reading and writing files
16. Advanced class features

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_s creates a copy of to_s under the name original_to_s.
  • The problem: if two plugins use alias on the same method, the second overwrites the first. This is fragile and a source of bugs that are hard to diagnose.
  • alias is 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.
  • super in 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. Each super calls 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 DamageLog is now at the head of the chain, before Pokemon.
  • When we call pikachu.take_damage(30), Ruby looks for the method in order: first in DamageLog, then in Pokemon.
  • super in DamageLog passes 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) in MagicShield passes the reduced damage to DamageLog, which calls super toward Pokemon.
  • This is what makes prepend so 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, ...]
MethodPosition in ancestorsCalled viaUsage
includeAfter the classInstance (pikachu.fiche)Adding utilities
prependBefore the classInstance (pikachu.take_damage)Intercepting & enriching
extendOutside the chainClass (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.
  • alias is the old technique, fragile and obsolete. Do not use it anymore.
  • prepend inserts a module before the class in .ancestors. super calls the original method.
  • Multiple prepended modules stack without conflict. Each super passes to the next one.
  • include adds after the class (utilities). prepend adds before (interception). extend adds class methods.
  • Always use prepend with super to extend existing code.