Il est connu que dans Ruby, les méthodes de classe sont héritées:
class P
def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works
Cependant, cela m'étonne que cela ne fonctionne pas avec les mixins:
module M
def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!
Je sais que la méthode #extend peut le faire:
module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works
Mais j'écris un mixin (ou, plutôt, j'aimerais écrire) contenant à la fois des méthodes d'instance et des méthodes de classe:
module Common
def self.class_method; puts "class method here" end
def instance_method; puts "instance method here" end
end
Maintenant, ce que je voudrais faire, c'est ceci:
class A; include Common
# custom part for A
end
class B; include Common
# custom part for B
end
Je veux que A, B héritent à la fois des méthodes d'instance et de classe du module Common
. Mais, bien sûr, cela ne fonctionne pas. Alors, n'y a-t-il pas un moyen secret de faire fonctionner cet héritage à partir d'un seul module?
Il me semble inélégant de diviser cela en deux modules différents, l'un à inclure, l'autre à étendre. Une autre solution possible serait d'utiliser une classe Common
au lieu d'un module. Mais ce n'est qu'une solution de contournement. (Et s'il y a deux ensembles de fonctionnalités communes Common1
et Common2
et nous avons vraiment besoin d'avoir des mixins?) Y a-t-il une raison profonde pour laquelle l'héritage des méthodes de classe ne fonctionne pas à partir des mixins?
Un idiome courant consiste à utiliser included
hook et injecter des méthodes de classe à partir de là.
module Foo
def self.included base
base.send :include, InstanceMethods
base.extend ClassMethods
end
module InstanceMethods
def bar1
'bar1'
end
end
module ClassMethods
def bar2
'bar2'
end
end
end
class Test
include Foo
end
Test.new.bar1 # => "bar1"
Test.bar2 # => "bar2"
Voici l'histoire complète, expliquant les concepts de métaprogrammation nécessaires pour comprendre pourquoi l'inclusion de modules fonctionne comme elle le fait dans Ruby.
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 méthode ancestors
:
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 instance avec le nom fourni. Puisque nous avons inclus M
dans C
, M
est maintenant un ancêtre de C
, donc quand on appelle foo
sur une instance de C
, Ruby trouvera cette méthode dans M
:
C.new.foo
#=> "foo"
Notez que l'inclusion ne copie aucune méthode d'instance ou de classe dans la classe - elle ajoute simplement une "note" à la classe qu'elle doit également rechercher méthodes d'instance dans le module inclus.
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
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"
Toujours dans Ruby, vous avez la possibilité de définir des méthodes singleton dites sur les objets. Ces méthodes sont ajoutées en tant que nouvelles méthodes d'instance à la classe singleton spéciale 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 aussi des objets simples? En fait, ils le sont! Est-ce à dire 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 consiste à 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]
Comme nous venons de l'établir, les méthodes de classe ne sont en fait que des méthodes d'instance sur la classe singleton de l'objet classe. Est-ce à dire 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
la ligne n'a pas l'air très sympa, donc Ruby ajouté Object#extend
, qui fait de même - c'est-à-dire qui 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!
extend
dans le moduleCet exemple précédent n'est pas un code bien structuré, pour deux raisons:
include
et extend
dans la définition HostClass
pour que notre module soit correctement inclus. Cela peut devenir très lourd si vous devez inclure de nombreux modules similaires.HostClass
fait directement référence à M::ClassMethods
, qui est un détail d'implémentation du module M
dont HostClass
ne devrait pas avoir besoin de connaître ou de se soucier.Alors qu'en est-il: lorsque nous appelons include
sur la première ligne, nous informons en quelque sorte le module qu'il a été inclus, et lui donnons également notre objet classe, afin qu'il puisse appeler lui-même extend
. De cette façon, c'est le travail du module d'ajouter les méthodes de classe s'il le souhaite.
C'est exactement ce que le spécial self.included
la méthode est pour. Ruby appelle automatiquement cette méthode chaque fois que le module est inclus dans une autre classe (ou module), et passe dans l'objet classe Host 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 dans self.included
. Nous avons l'objet class, nous pouvons donc appeler n'importe quelle autre méthode (class) dessus:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end
Comme Sergio l'a mentionné dans les commentaires, pour les gars qui sont déjà en Rails (ou cela ne me dérange pas en fonction de Active Support ), Concern
est utile ici:
require 'active_support/concern'
module Common
extend ActiveSupport::Concern
def instance_method
puts "instance method here"
end
class_methods do
def class_method
puts "class method here"
end
end
end
class A
include Common
end
Vous pouvez avoir votre gâteau et le manger aussi en faisant ceci:
module M
def self.included(base)
base.class_eval do # do anything you would do at class level
def self.doit #class method
@@fred = "Flintstone"
"class method doit called"
end # class method define
def doit(str) #instance method
@@common_var = "all instances"
@instance_var = str
"instance method doit called"
end
def get_them
[@@common_var,@instance_var,@@fred]
end
end # class_eval
end # included
end # module
class F; end
F.include M
F.doit # >> "class method doit called"
a = F.new
b = F.new
a.doit("Yo") # "instance method doit called"
b.doit("Ho") # "instance method doit called"
a.get_them # >> ["all instances", "Yo", "Flintstone"]
b.get_them # >> ["all instances", "Ho", "Flintstone"]
Si vous avez l'intention d'ajouter des variables d'instance et de classe, vous finirez par vous arracher les cheveux car vous rencontrerez un tas de code cassé, sauf si vous le faites de cette façon.