web-dev-qa-db-fra.com

Héritage des méthodes de classe des modules / mixins dans Ruby

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?

86
Boris Stitnicky

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"
160
Sergio Tulentsev

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.

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 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.

Qu'en est-il des méthodes "classe" dans 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"

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]

Comment inclure les méthodes de classe dans un module?

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!

Déplacer l'appel extend dans le module

Cet exemple précédent n'est pas un code bien structuré, pour deux raisons:

  1. Nous devons maintenant appeler les deuxinclude 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.
  2. 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
36
Máté Solymosi

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
6
Franklin Yu

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.

2
Bryan Colvin