Aller au contenu principal
Voir
Chapitre 16 / 16Les fonctionnalités avancées des classes
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

Les fonctionnalités avancées des classes

Ce chapitre présente des fonctionnalités avancées des classes : la surcharge d'opérateurs, le duck typing, Struct et la gestion de l'identité des objets. Ces outils rendent les classes plus expressives et naturelles à utiliser.

Principe

En Ruby, les opérateurs comme ==, +, [] sont en réalité des méthodes. Écrire a == b revient à appeler a.==(b). On peut donc les redéfinir dans nos propres classes pour leur donner un comportement logique.

Ruby repose aussi sur le duck typing : ce qui compte n'est pas le type d'un objet, mais les méthodes auxquelles il répond. "Si ça marche comme un canard et que ça fait coin-coin comme un canard, alors c'est un canard."

Surcharger == (égalité)

Par défaut, == vérifie si deux variables pointent vers le même objet en mémoire. Ce n'est généralement pas ce qu'on veut :

class Pokemon
attr_reader :name, :level

def initialize(name, level)
@name = name
@level = level
end
end

a = Pokemon.new('Pikachu', 25)
b = Pokemon.new('Pikachu', 25)
puts a == b # => false (deux objets différents en mémoire !)

Pour comparer par valeur, on redéfinit == :

class Pokemon
attr_reader :name, :level

def initialize(name, level)
@name = name
@level = level
end

def ==(other)
return false unless other.is_a?(Pokemon)

return @name == other.name && @level == other.level
end
end

a = Pokemon.new('Pikachu', 25)
b = Pokemon.new('Pikachu', 25)
puts a == b # => true (mêmes valeurs)
  • other.is_a?(Pokemon) vérifie le type pour éviter les erreurs si on compare un Pokémon avec autre chose.
  • On compare les attributs qui définissent l'identité logique de l'objet.

Surcharger [] et []= (accès par clé)

On a vu que pikachu[:attack] fonctionne quand pikachu est un Hash. Mais si pikachu est un objet Pokemon, ça ne marche pas par défaut. On peut changer ça en définissant les méthodes [] et []=.

L'idée : un Pokémon a plusieurs stats (PV, attaque, défense...) stockées dans un Hash interne. Plutôt que d'écrire pikachu.stats[:attack], on aimerait écrire directement pikachu[:attack], comme si le Pokémon lui-même était un Hash.

class Pokemon
attr_reader :name, :level

def initialize(name, level, stats)
@name = name
@level = level
@stats = stats
end

# Quand on écrit pikachu[:attack], Ruby appelle cette méthode
def [](stat_name)
return @stats[stat_name]
end

# Quand on écrit pikachu[:attack] = 60, Ruby appelle cette méthode
def []=(stat_name, value)
@stats[stat_name] = value
return value
end
end

pikachu = Pokemon.new('Pikachu', 25, { hp: 35, attack: 55, defense: 40 })
puts pikachu[:attack] # => 55

pikachu[:attack] = 60
puts pikachu[:attack] # => 60
  • def [](stat_name) est une méthode dont le nom est []. La syntaxe est inhabituelle, mais c'est bien un nom de méthode comme un autre. Quand Ruby voit pikachu[:attack], il traduit ça en pikachu.[](: attack).
  • def []=(stat_name, value) fonctionne de la même façon : pikachu[:attack] = 60 devient pikachu.[]=(:attack, 60).
  • C'est le même principe que le setter level= vu au chapitre 8 : Ruby traduit une syntaxe naturelle en appel de méthode.

Duck typing et respond_to?

Le duck typing signifie qu'on ne vérifie pas le type d'un objet, mais les méthodes qu'il possède :

def display_creature(creature)
puts "#{creature.name} Niv.#{creature.level}" if creature.respond_to?(:name)
end
  • respond_to?(:name) retourne true si l'objet possède une méthode name, quelle que soit sa classe.
  • Cela permet à une méthode d'accepter n'importe quel objet qui a les bonnes méthodes, pas seulement des instances d'une classe précise.

Struct — classes de données rapides

Struct crée automatiquement une classe avec constructeur, getters, setters, == et to_s :

Move = Struct.new(:name, :type, :power, :accuracy)

thunderbolt = Move.new('Tonnerre', :electric, 90, 100)
puts thunderbolt.name # => Tonnerre
puts thunderbolt.power # => 90

# == compare automatiquement toutes les valeurs
other = Move.new('Tonnerre', :electric, 90, 100)
puts thunderbolt == other # => true
  • Struct.new crée une classe complète en une seule ligne. C'est parfait pour les objets de données simples comme les attaques.
  • On peut ajouter des méthodes au Struct avec un bloc :
Move = Struct.new(:name, :type, :power, :accuracy) do
def to_s
return "#{name} (#{type}) — Puissance : #{power}"
end
end

puts Move.new('Tonnerre', :electric, 90, 100)
# => Tonnerre (electric) — Puissance : 90

method_missing — interception dynamique

method_missing est appelée quand on invoque une méthode qui n'existe pas. On peut l'utiliser pour créer des accesseurs dynamiques :

  • method_missing intercepte l'appel. Si le nom correspond à une stat connue, on retourne sa valeur. Sinon, super propage l'erreur normalement.
  • Toujours implémenter respond_to_missing? en parallèle. Sans elle, respond_to? retournerait false même si method_missing gère l'appel.
  • Ce mécanisme est puissant mais à utiliser avec parcimonie : il rend le code plus difficile à comprendre. Préférer les méthodes explicites quand c'est possible.

Identité des objets

Ruby a plusieurs niveaux de comparaison :

a = 'Pikachu'
b = 'Pikachu'

puts a == b # => true (même valeur)
puts a.equal?(b) # => false (objets différents en mémoire)
puts a.eql?(b) # => true (même valeur et même type)
  • == : égalité par valeur (la plus courante, celle qu'on redéfinit)
  • .equal? : identité mémoire (même objet). Ne jamais la redéfinir.
  • .eql? : utilisée par les Hash pour les clés. Si on redéfinit ==, redéfinir aussi eql? et hash :
class Pokemon
def ==(other)
return false unless other.is_a?(Pokemon)

return @name == other.name && @level == other.level
end

def eql?(other)
return self == other
end

def hash
return [@name, @level].hash
end
end
  • eql? et hash doivent être cohérents avec == : deux objets égaux doivent avoir le même hash.
  • Sans ça, utiliser un Pokémon comme clé de Hash donnerait des résultats incohérents.

freeze, dup et clone

freeze rend un objet immutable :

name = 'Pikachu'
name.freeze
puts name.frozen? # => true
# name.upcase! # => Erreur ! FrozenError

dup crée une copie superficielle :

original = { name: 'Pikachu', stats: { hp: 35 } }
copy = original.dup

copy[:name] = 'Raichu' # Ne modifie PAS l'original
copy[:stats][:hp] = 999 # MODIFIE l'original aussi !
  • dup copie le Hash lui-même, mais les valeurs imbriquées (comme le Hash stats) sont partagées. C'est une copie "superficielle".
  • clone est similaire à dup mais préserve aussi l'état frozen.
  • Pour une copie profonde, on peut utiliser Marshal.dump + Marshal.load (vu au chapitre 13).

Note : dans certains projets Ruby, on ne met pas .freeze sur les constantes car cela peut empêcher certaines techniques d'extension du code. C'est un choix de conception.

Conclusion

  • Les opérateurs Ruby (==, [], []=, <=>, to_s) sont des méthodes qu'on peut redéfinir.
  • Redéfinir == pour comparer par valeur. Toujours vérifier le type avec is_a?.
  • respond_to? vérifie les capacités d'un objet plutôt que son type (duck typing).
  • Struct crée des classes de données complètes en une ligne.
  • method_missing intercepte les appels à des méthodes inexistantes. Toujours implémenter respond_to_missing?. À utiliser avec parcimonie.
  • Si on redéfinit ==, redéfinir aussi eql? et hash pour la cohérence avec les Hash.
  • freeze rend un objet immutable. dup crée une copie superficielle.