Voici l'histoire complète, expliquant les concepts de métaprogrammation nécessaires pour comprendre pourquoi l'inclusion de module fonctionne comme elle le fait dans Ruby.
Que se passe-t-il lorsqu'un module est inclus?
L'inclusion d'un module dans une classe ajoute le module aux ancêtres de la classe. Vous pouvez regarder les ancêtres de n'importe quelle classe ou module en appelant sa ancestors
méthode:
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
# ^ look, it's right here!
Lorsque vous appelez une méthode sur une instance de C
, Ruby examinera chaque élément de cette liste d'ancêtres afin de trouver une méthode d'instance avec le nom fourni. Puisque nous avons inclus M
dans C
, M
est maintenant un ancêtre de C
, donc lorsque nous appelons foo
une instance de C
, Ruby trouvera cette méthode dans M
:
C.new.foo
#=> "foo"
Notez que l'inclusion ne copie aucune instance ou méthode de classe dans la classe - elle ajoute simplement une "note" à la classe indiquant qu'elle doit également rechercher des méthodes d'instance dans le module inclus.
Qu'en est-il des méthodes "class" de notre module?
Parce que l'inclusion ne change que la façon dont les méthodes d'instance sont distribuées, l'inclusion d'un module dans une classe ne rend ses méthodes d'instance disponibles que sur cette classe. Les méthodes "class" et autres déclarations du module ne sont pas automatiquement copiées dans la classe:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
#=> "bar"
C.new.instance_method
#=> "foo"
C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class
Comment Ruby implémente-t-il les méthodes de classe?
Dans Ruby, les classes et les modules sont des objets simples - ce sont des instances de la classe Class
et Module
. Cela signifie que vous pouvez créer dynamiquement de nouvelles classes, les affecter à des variables, etc.:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Aussi dans Ruby, vous avez la possibilité de définir des méthodes dites singleton sur des objets. Ces méthodes sont ajoutées en tant que nouvelles méthodes d'instance à la classe singleton spéciale et cachée de l'objet:
obj = Object.new
# define singleton method
def obj.foo
"foo"
end
# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]
Mais les classes et les modules ne sont-ils pas également de simples objets? En fait, ils le sont! Cela signifie-t-il qu'ils peuvent également avoir des méthodes singleton? Oui! Et c'est ainsi que naissent les méthodes de classe:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Ou, la manière la plus courante de définir une méthode de classe est de l'utiliser self
dans le bloc de définition de classe, qui fait référence à l'objet de classe en cours de création:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Comment inclure les méthodes de classe dans un module?
Comme nous venons de l'établir, les méthodes de classe ne sont en réalité que des méthodes d'instance sur la classe singleton de l'objet de classe. Cela signifie-t-il que nous pouvons simplement inclure un module dans la classe singleton pour ajouter un tas de méthodes de classe? Oui!
module M
def new_instance_method; "hi"; end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
self.singleton_class.include M::ClassMethods
end
HostKlass.new_class_method
#=> "hello"
Cette self.singleton_class.include M::ClassMethods
ligne n'a pas l'air très jolie, donc Ruby a ajouté Object#extend
, qui fait de même - c'est-à-dire inclut un module dans la classe singleton de l'objet:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Déplacer l' extend
appel dans le module
Cet exemple précédent n'est pas un code bien structuré, pour deux raisons:
- Nous devons maintenant appeler les deux
include
et extend
dans la HostClass
définition pour que notre module soit correctement inclus. Cela peut devenir très fastidieux si vous devez inclure de nombreux modules similaires.
HostClass
références directes M::ClassMethods
, qui est un détail de mise en œuvre du module M
qui HostClass
ne devrait pas avoir besoin de connaître ou de se soucier.
Alors que diriez-vous de ceci: lorsque nous appelons include
sur la première ligne, nous notifions en quelque sorte le module qu'il a été inclus, et lui donnons également notre objet de classe, afin qu'il puisse s'appeler extend
lui - même. De cette façon, c'est le travail du module d'ajouter les méthodes de classe s'il le souhaite.
C'est exactement à cela que sert la méthode spécialeself.included
. Ruby appelle automatiquement cette méthode chaque fois que le module est inclus dans une autre classe (ou module), et passe l'objet de classe hôte comme premier argument:
module M
def new_instance_method; "hi"; end
def self.included(base) # `base` is `HostClass` in our case
base.extend ClassMethods
end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
def self.existing_class_method; "cool"; end
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ still there!
Bien sûr, l'ajout de méthodes de classe n'est pas la seule chose que nous pouvons faire self.included
. Nous avons l'objet de classe, nous pouvons donc appeler n'importe quelle autre méthode (de classe) dessus:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end