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 voitpikachu[:attack], il traduit ça enpikachu.[](: attack).def []=(stat_name, value)fonctionne de la même façon :pikachu[:attack] = 60devientpikachu.[]=(: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)retournetruesi l'objet possède une méthodename, 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.newcré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_missingintercepte l'appel. Si le nom correspond à une stat connue, on retourne sa valeur. Sinon,superpropage l'erreur normalement.- Toujours implémenter
respond_to_missing?en parallèle. Sans elle,respond_to?retourneraitfalsemême simethod_missinggè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 aussieql?ethash:
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?ethashdoivent être cohérents avec==: deux objets égaux doivent avoir le mêmehash.- 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 !
dupcopie le Hash lui-même, mais les valeurs imbriquées (comme le Hashstats) sont partagées. C'est une copie "superficielle".cloneest similaire àdupmais préserve aussi l'étatfrozen.- 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 avecis_a?. respond_to?vérifie les capacités d'un objet plutôt que son type (duck typing).Structcrée des classes de données complètes en une ligne.method_missingintercepte les appels à des méthodes inexistantes. Toujours implémenterrespond_to_missing?. À utiliser avec parcimonie.- Si on redéfinit
==, redéfinir aussieql?ethashpour la cohérence avec les Hash. freezerend un objet immutable.dupcrée une copie superficielle.