10-10-2009, 09:50 AM
(Modification du message : 10-10-2009, 10:18 AM par Sephi-Chan.)
La délégation des méthodes
ActiveSupport intègre un composant bien sympathique à la classe Module : la méthode delegate.
Cette méthode permet, comme son nom l'indique, de déléguer son travail à une autre méthode.
Le cas d'exemple est un jeu (Bugspirit) découpé en parties (les univers) pouvant accueillir un certain nombre de groupes de joueurs (les nuées).
Ce nombre de groupe est déterminé par la carte associée à l'univers. Une petite carte pourra accueillir un nombre restreint de groupe qu'une carte immense.
Et c'est là qu'entre en jeu le délégué : quand on demandera le nombre de joueurs qu'un univers peut accueillir, il ira demander cette information à sa carte.
Voici donc une partie de la classe Universe (très épurée, pour se focaliser sur l'exemple) :
On précise que notre univers est associé à une carte grâce à une relation belongs_to :map (Map étant un autre modèle). Notre table universes contient une colonne map_id. Cette ligne ajoute (entre autre) à nos instances de la classe Universe une méthode map qui va chercher la carte associé à l'univers dans la base de données.
Grâce à la ligne delegate :maximum_number_of_swarms, :to => :map, on demande à nos instance de la classe Universe de répondre à la méthode maximum_number_of_swarms en appelant à son tour cette méthode sur l'objet map et en retournant le résultat.
Ainsi, les codes qui suivent sont équivalents :
Dans notre cas, à chaque fois qu'on ajoutera un nuée (Swarm) aux nuées associées à cet univers, le callback after_add sera appelé et vérifiera si — en comptant la nuée ajoutée — l'univers a accueilli le nombre maximal de groupes permis par la carte. Si c'est le cas, le flag qui indique si la partie est pleine, ce qui par ailleurs conduira au lancement de la partie grâce au callback after_update tart_the_game, :if => :full_changed?.
Notez que les méthodes délégués peuvent être délégués à des variables de classes, d'instance ou des constantes appartenant elle-mêmes à la classe en cours ou autre.
On peut également préfixer la méthode. Ainsi, si je veux connaître l'adresse de livraison d'une commande (qui est en fait l'adresse de la personne qui a commandé), appelée @order.person_address a plus de sens que d'appeller @order.address.
Notez que le préfixe peut être donné explicitement, ainsi, plutôt que de parler de l'adresse de la personne, on peut parler de l'adresse d'un client.
On peut aussi déléguer plusieurs méthodes en même temps :
Enfin, parlons un peu de la robustesse du code.
Souvenez-vous de l'exemple de mon jeu. Quand on appelle la méthode maximum_number_of_swarms, elle est en fait appelée sur la carte associée à l'univers. Si cette carte n'existe pas, Ruby cherchera à appeler la méthode sur un objet inexistant (nil, puisque c'est ce que renverra la méthode map si l'univers n'a pas de carte) !
Pour éviter cela, on peut procéder à une petite modification :
Ainsi, si on vient à demander le nombre de nuées que peut accueillir un univers alors que celui-ci n'a pas de carte associée, alors il se contentera simplement de répondre nil.
Voilà pour les délégués !
Si ce sujet vous a intéressé — que Ruby vous tente ou non — je vous serais reconnaissant de me donner votre ressenti afin que je puisse l'améliorer.
Sephi-Chan, qui ne pensait pas en écrire autant quand il a commencé…
ActiveSupport intègre un composant bien sympathique à la classe Module : la méthode delegate.
Cette méthode permet, comme son nom l'indique, de déléguer son travail à une autre méthode.
Le cas d'exemple est un jeu (Bugspirit) découpé en parties (les univers) pouvant accueillir un certain nombre de groupes de joueurs (les nuées).
Ce nombre de groupe est déterminé par la carte associée à l'univers. Une petite carte pourra accueillir un nombre restreint de groupe qu'une carte immense.
Et c'est là qu'entre en jeu le délégué : quand on demandera le nombre de joueurs qu'un univers peut accueillir, il ira demander cette information à sa carte.
Voici donc une partie de la classe Universe (très épurée, pour se focaliser sur l'exemple) :
class Universe < ActiveRecord::Base
belongs_to :map
has_many :participations
has_many warms, :through => :participations,
:after_add => :try_to_flag_as_full
before_validation :default_locale
after_create :default_name
after_update tart_the_game, :if => :full_changed?
# full_changed? is an auto generated method. If the attribute "full" change during
# an update, the method start_the_game is executed.
delegate :maximum_number_of_swarms, :to => :map
protected
def try_to_flag_as_full
if swarms.count == maximum_number_of_swarms
self.full = true
save!
end
end
end
On précise que notre univers est associé à une carte grâce à une relation belongs_to :map (Map étant un autre modèle). Notre table universes contient une colonne map_id. Cette ligne ajoute (entre autre) à nos instances de la classe Universe une méthode map qui va chercher la carte associé à l'univers dans la base de données.
Grâce à la ligne delegate :maximum_number_of_swarms, :to => :map, on demande à nos instance de la classe Universe de répondre à la méthode maximum_number_of_swarms en appelant à son tour cette méthode sur l'objet map et en retournant le résultat.
Ainsi, les codes qui suivent sont équivalents :
@universe = Universe.find(1) # On récupère le territoire qui a 1 pour ID.
# Ceci :
@universe.maximum_number_of_swarms
# Est équivalent à :
@universe.map.maximum_number_of_swarms
Dans notre cas, à chaque fois qu'on ajoutera un nuée (Swarm) aux nuées associées à cet univers, le callback after_add sera appelé et vérifiera si — en comptant la nuée ajoutée — l'univers a accueilli le nombre maximal de groupes permis par la carte. Si c'est le cas, le flag qui indique si la partie est pleine, ce qui par ailleurs conduira au lancement de la partie grâce au callback after_update tart_the_game, :if => :full_changed?.
Notez que les méthodes délégués peuvent être délégués à des variables de classes, d'instance ou des constantes appartenant elle-mêmes à la classe en cours ou autre.
class Foo
CONSTANT_ARRAY = [ 0, 1, 2, 3 ]
@@class_array = [ 4, 5, 6, 7 ]
def initialize
@instance_array = [ 8, 9, 10, 11 ]
end
delegate um, :to => :CONSTANT_ARRAY
delegate :min, :to => @class_array
delegate :max, :to => instance_array
end
Foo.new.sum # => 6
Foo.new.min # => 4
Foo.new.max # => 11
On peut également préfixer la méthode. Ainsi, si je veux connaître l'adresse de livraison d'une commande (qui est en fait l'adresse de la personne qui a commandé), appelée @order.person_address a plus de sens que d'appeller @order.address.
class Order < ActiveRecord::Base
belongs_to :person
delegate :address, :to => :client, :prefix => true
end
@order = Order.find(1)
@order.address # Non !
@order.person_address # Oui !
Notez que le préfixe peut être donné explicitement, ainsi, plutôt que de parler de l'adresse de la personne, on peut parler de l'adresse d'un client.
class Order < ActiveRecord::Base
belongs_to :person
delegate :address, :to => :client, :prefix => :customer
end
@order = Order.find(1)
@order.address # Non !
@order.person_address # Non plus !
@order.person.address # Oui on peut, on utilise pas de délégué en faisant ça.
@order.customer_address # Oui !
On peut aussi déléguer plusieurs méthodes en même temps :
class Order < ActiveRecord::Base
belongs_to :person
delegate :address, :name, :to => :client, :prefix => :customer
end
Enfin, parlons un peu de la robustesse du code.
Souvenez-vous de l'exemple de mon jeu. Quand on appelle la méthode maximum_number_of_swarms, elle est en fait appelée sur la carte associée à l'univers. Si cette carte n'existe pas, Ruby cherchera à appeler la méthode sur un objet inexistant (nil, puisque c'est ce que renverra la méthode map si l'univers n'a pas de carte) !
Pour éviter cela, on peut procéder à une petite modification :
class Universe < ActiveRecord::Base
belongs_to :map
delegate :maximum_number_of_swarms, :to => :map, :allow_nil => true
end
Ainsi, si on vient à demander le nombre de nuées que peut accueillir un univers alors que celui-ci n'a pas de carte associée, alors il se contentera simplement de répondre nil.
Voilà pour les délégués !
Si ce sujet vous a intéressé — que Ruby vous tente ou non — je vous serais reconnaissant de me donner votre ressenti afin que je puisse l'améliorer.
Sephi-Chan, qui ne pensait pas en écrire autant quand il a commencé…