Étendre du code avec prepend
Ce chapitre présente les techniques pour étendre du code existant sans le modifier directement. C'est un concept fondamental en Ruby : on peut enrichir une classe après sa création, même si on n'a pas accès au fichier source.
Principe
Imaginons qu'on utilise une classe Pokemon écrite par quelqu'un d'autre. On veut ajouter un système de log qui affiche un message à chaque fois qu'un Pokémon subit des dégâts. Mais on ne veut pas (et parfois on ne peut pas) modifier le fichier original.
Ruby offre trois solutions :
- Réouverture de classes : on rouvre la classe pour ajouter ou modifier des méthodes. Simple mais risqué si on écrase une méthode existante.
- alias : l'ancienne technique pour sauvegarder une méthode avant de la redéfinir. Fragile.
- prepend : la technique moderne et sûre. On insère un module avant la classe dans la chaîne d'ancêtres.
Réouverture de classes
En Ruby, les classes ne sont jamais "fermées". On peut les rouvrir pour ajouter des méthodes :
class Pokemon
attr_reader :name, :level
def initialize(name, level)
@name = name
@level = level
end
def to_s
return "#{@name} Niv.#{@level}"
end
end
# Plus loin dans le code (ou dans un autre fichier)
class Pokemon
def can_evolve?
return @level >= 16
end
end
pikachu = Pokemon.new('Pikachu', 25)
puts pikachu.can_evolve? # => true
puts pikachu # => Pikachu Niv.25 (to_s fonctionne toujours)
- Ajouter une méthode en rouvrant la classe est sans risque. L'original n'est pas touché.
- Redéfinir une méthode existante est risqué : le comportement original est perdu.
alias — l'ancienne technique (à éviter)
alias sauvegarde une méthode sous un autre nom avant de la redéfinir :
class Pokemon
alias original_to_s to_s
def to_s
return "[Modifié] #{original_to_s}"
end
end
puts Pokemon.new('Pikachu', 25) # => [Modifié] Pikachu Niv.25
alias original_to_s to_scrée une copie deto_ssous le nomoriginal_to_s.- Le problème : si deux plugins font
aliassur la même méthode, le second écrase le premier. C'est fragile et source de bugs difficiles à diagnostiquer. aliasest une technique historique. On ne l'utilise plus en Ruby moderne.
prepend — la technique moderne
prepend insère un module avant la classe dans la chaîne d'ancêtres. Le module peut appeler super pour exécuter la méthode originale :
Affiche :
Pikachu subit 30 dégâts !
Pikachu a 70 PV restants.
superdans le module appelle la méthode de la classe originale. Le comportement original est préservé et enrichi.- Le module peut exécuter du code avant et après
super. - Contrairement à
alias, plusieurs modules peuvent être prependés sans conflit. Chaquesuperappelle le suivant dans la chaîne.
La chaîne d'ancêtres avec prepend
prepend modifie l'ordre dans .ancestors :
puts Pokemon.ancestors.inspect
# Avant : [Pokemon, Object, Kernel, BasicObject]
Pokemon.prepend(DamageLog)
puts Pokemon.ancestors.inspect
# Après : [DamageLog, Pokemon, Object, Kernel, BasicObject]
- Le module
DamageLogest maintenant en tête de la chaîne, avantPokemon. - Quand on appelle
pikachu.take_damage(30), Ruby cherche la méthode dans l'ordre : d'abord dansDamageLog, puis dansPokemon. superdansDamageLogpasse au prochain élément de la chaîne :Pokemon.
Plusieurs prepend
On peut prependre plusieurs modules. Chaque super remonte la chaîne :
module DamageLog
def take_damage(amount)
puts "[LOG] #{name} subit #{amount} dégâts"
super
end
end
module MagicShield
def take_damage(amount)
reduced_damage = (amount * 0.8).to_i
puts "[BOUCLIER] Dégâts réduits de #{amount} à #{reduced_damage}"
super(reduced_damage)
end
end
Pokemon.prepend(DamageLog)
Pokemon.prepend(MagicShield)
puts Pokemon.ancestors.inspect
# => [MagicShield, DamageLog, Pokemon, Object, ...]
- Le dernier module prependé est en tête. L'ordre d'appel est :
MagicShield→DamageLog→Pokemon. super(reduced_damage)dansMagicShieldpasse les dégâts réduits àDamageLog, qui appellesuperversPokemon.- C'est ce qui rend
prependsi puissant : chaque plugin s'empile sans casser les autres.
include vs prepend vs extend
Récapitulatif des trois façons d'injecter un module :
class Pokemon
include Displayable # Après la classe (utilitaires)
prepend DamageLog # Avant la classe (interception)
extend Search # Méthodes de classe
end
puts Pokemon.ancestors.inspect
# => [DamageLog, Pokemon, Displayable, Object, ...]
| Méthode | Position dans ancestors | Appel via | Usage |
|---|---|---|---|
include | Après la classe | Instance (pikachu.fiche) | Ajouter des utilitaires |
prepend | Avant la classe | Instance (pikachu.take_damage) | Intercepter et enrichir |
extend | Hors de la chaîne | Classe (Pokemon.search(...)) | Méthodes de classe |
include: le module s'insère après la classe. La classe a la priorité. C'est pour le partage de comportement (Enumerable, Comparable).prepend: le module s'insère avant la classe. Le module a la priorité. C'est pour l'interception et l'extension.extend: les méthodes deviennent des méthodes de classe. Pas dans.ancestorsdes instances.
Règle importante
Ne jamais remplacer une classe existante par une autre hiérarchie. Toujours utiliser prepend pour ajouter du comportement :
# Ne PAS faire
class Pokemon
def take_damage(amount)
# réécriture complète, le comportement original est perdu
end
end
# Faire à la place
module MyExtension
def take_damage(amount)
# code ajouté
super # le comportement original est préservé
end
end
Pokemon.prepend(MyExtension)
Conclusion
- Les classes Ruby sont ouvertes. Ajouter des méthodes en rouvrant est sûr. Écraser des méthodes existantes perd le comportement original.
aliasest l'ancienne technique, fragile et obsolète. Ne plus l'utiliser.prependinsère un module avant la classe dans.ancestors.superappelle la méthode originale.- Plusieurs modules prependés s'empilent sans conflit. Chaque
superpasse au suivant. includeajoute après la classe (utilitaires).prependajoute avant (interception).extendajoute des méthodes de classe.- Toujours utiliser
prependavecsuperpour étendre du code existant.