Aller au contenu principal
Voir
Chapitre 11 / 16Étendre du code avec prepend
1. Les variables et les types de données
2. Les chaînes et les symboles
3. Les tableaux
4. Les tables associatives
5. Les conditions
6. Les boucles et les itérateurs
7. Les méthodes et les blocs
8. Les classes et les objets
9. L'héritage
10. Les modules
11. Étendre du code avec prepend
12. Enumerable
13. Pour aller plus loin
14. La gestion des erreurs
15. Lire et écrire des fichiers
16. Les fonctionnalités avancées des classes

É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_s crée une copie de to_s sous le nom original_to_s.
  • Le problème : si deux plugins font alias sur la même méthode, le second écrase le premier. C'est fragile et source de bugs difficiles à diagnostiquer.
  • alias est 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.
  • super dans 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. Chaque super appelle 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 DamageLog est maintenant en tête de la chaîne, avant Pokemon.
  • Quand on appelle pikachu.take_damage(30), Ruby cherche la méthode dans l'ordre : d'abord dans DamageLog, puis dans Pokemon.
  • super dans DamageLog passe 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 : MagicShieldDamageLogPokemon.
  • super(reduced_damage) dans MagicShield passe les dégâts réduits à DamageLog, qui appelle super vers Pokemon.
  • C'est ce qui rend prepend si 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éthodePosition dans ancestorsAppel viaUsage
includeAprès la classeInstance (pikachu.fiche)Ajouter des utilitaires
prependAvant la classeInstance (pikachu.take_damage)Intercepter et enrichir
extendHors de la chaîneClasse (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 .ancestors des 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.
  • alias est l'ancienne technique, fragile et obsolète. Ne plus l'utiliser.
  • prepend insère un module avant la classe dans .ancestors. super appelle la méthode originale.
  • Plusieurs modules prependés s'empilent sans conflit. Chaque super passe au suivant.
  • include ajoute après la classe (utilitaires). prepend ajoute avant (interception). extend ajoute des méthodes de classe.
  • Toujours utiliser prepend avec super pour étendre du code existant.