Erben von Klassenmethoden aus Modulen / Mixins in Ruby

95

Es ist bekannt, dass in Ruby Klassenmethoden vererbt werden:

class P
  def self.mm; puts 'abc' end
end
class Q < P; end
Q.mm # works

Es überrascht mich jedoch, dass es mit Mixins nicht funktioniert:

module M
  def self.mm; puts 'mixin' end
end
class N; include M end
M.mm # works
N.mm # does not work!

Ich weiß, dass die Methode #extend dies kann:

module X; def mm; puts 'extender' end end
Y = Class.new.extend X
X.mm # works

Aber ich schreibe ein Mixin (oder möchte lieber schreiben), das sowohl Instanzmethoden als auch Klassenmethoden enthält:

module Common
  def self.class_method; puts "class method here" end
  def instance_method; puts "instance method here" end
end

Was ich jetzt tun möchte, ist Folgendes:

class A; include Common
  # custom part for A
end
class B; include Common
  # custom part for B
end

Ich möchte, dass A, B sowohl Instanz- als auch Klassenmethoden vom CommonModul erben . Aber das funktioniert natürlich nicht. Gibt es nicht einen geheimen Weg, um diese Vererbung von einem einzigen Modul aus funktionieren zu lassen?

Es erscheint mir unelegant, dies in zwei verschiedene Module aufzuteilen, eines zum Einschließen, das andere zum Erweitern. Eine andere mögliche Lösung wäre die Verwendung einer Klasse Commonanstelle eines Moduls. Dies ist jedoch nur eine Problemumgehung. (Was ist, wenn es zwei Sätze gemeinsamer Funktionen gibt Common1und Common2wir wirklich Mixins benötigen?) Gibt es einen tiefen Grund, warum die Vererbung von Klassenmethoden von Mixins nicht funktioniert?

Boris Stitnicky
quelle
1
Mit dem Unterschied, dass ich hier weiß, dass es möglich ist - ich frage nach der am wenigsten hässlichen Art und Weise und nach den Gründen, warum die naive Wahl nicht funktioniert.
Boris Stitnicky
1
Mit mehr Erfahrung verstand ich, dass Ruby zu weit gehen würde, um die Absicht des Programmierers zu erraten, wenn durch das Einfügen eines Moduls auch die Modulmethoden zur Singleton-Klasse des Einschlusses hinzugefügt würden. Dies liegt daran, dass "Modulmethoden" in der Tat nichts anderes als Singleton-Methoden sind. Module sind nicht speziell für Singleton-Methoden, sondern speziell für Namespaces, in denen Methoden und Konstanten definiert sind. Der Namespace ist völlig unabhängig von den Singleton-Methoden eines Moduls, daher ist die Klassenvererbung von Singleton-Methoden erstaunlicher als das Fehlen in Modulen.
Boris Stitnicky

Antworten:

171

Eine gängige Redewendung ist die Verwendung von includedHook- und Inject-Klassenmethoden von dort.

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"
Sergio Tulentsev
quelle
26
includefügt Instanzmethoden hinzu, extendfügt Klassenmethoden hinzu. So funktioniert es. Ich sehe keine Inkonsistenz, nur unerfüllte Erwartungen :)
Sergio Tulentsev
1
Ich ertrage langsam die Tatsache, dass Ihr Vorschlag so elegant ist, wie es die praktische Lösung dieses Problems nur kann. Aber ich würde gerne wissen, warum etwas, das mit Klassen funktioniert, nicht mit Modulen funktioniert.
Boris Stitnicky
6
@ BorisStitnicky Vertraue dieser Antwort. Dies ist eine sehr verbreitete Redewendung in Ruby, die genau den Anwendungsfall löst, nach dem Sie fragen, und genau aus den Gründen, die Sie erlebt haben. Es mag "unelegant" aussehen, aber es ist die beste Wahl. (Wenn Sie dies häufig tun, können Sie die includedMethodendefinition in ein anderes Modul verschieben und DAS in Ihr Hauptmodul aufnehmen;)
Phrogz
2
Lesen Sie diesen Thread, um mehr über das "Warum?" .
Phrogz
2
@werkshy: Füge das Modul in eine Dummy-Klasse ein.
Sergio Tulentsev
47

Hier ist die ganze Geschichte, in der die notwendigen Metaprogrammierungskonzepte erläutert werden, die erforderlich sind, um zu verstehen, warum die Moduleinbeziehung so funktioniert wie in Ruby.

Was passiert, wenn ein Modul enthalten ist?

Durch das Einfügen eines Moduls in eine Klasse wird das Modul den Vorfahren der Klasse hinzugefügt. Sie können die Vorfahren jeder Klasse oder jedes Moduls anzeigen, indem Sie die folgende ancestorsMethode aufrufen :

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!

Wenn Sie eine Methode für eine Instanz von aufrufen C, überprüft Ruby jedes Element dieser Ahnenliste, um eine Instanzmethode mit dem angegebenen Namen zu finden. Da wir Min aufgenommen haben C, Mist jetzt ein Vorfahr von C. Wenn wir also fooeine Instanz von aufrufen C, findet Ruby diese Methode in M:

C.new.foo
#=> "foo"

Beachten Sie, dass durch die Aufnahme keine Instanz- oder Klassenmethoden in die Klasse kopiert werden. Sie fügt der Klasse lediglich einen "Hinweis" hinzu, nach dem auch Instanzmethoden im enthaltenen Modul gesucht werden sollen.

Was ist mit den "Klassen" -Methoden in unserem Modul?

Da durch die Aufnahme nur die Art und Weise geändert wird, in der Instanzmethoden versendet werden, werden durch das Einschließen eines Moduls in eine Klasse nur die Instanzmethoden für diese Klasse verfügbar . Die "Klassen" -Methoden und andere Deklarationen im Modul werden nicht automatisch in die Klasse kopiert:

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

Wie implementiert Ruby Klassenmethoden?

In Ruby sind Klassen und Module einfache Objekte - sie sind Instanzen der Klasse Classund Module. Dies bedeutet, dass Sie dynamisch neue Klassen erstellen, sie Variablen zuweisen können usw.:

klass = Class.new do
  def foo
    "foo"
  end
end
#=> #<Class:0x2b613d0>

klass.new.foo
#=> "foo"

Auch in Ruby haben Sie die Möglichkeit, sogenannte Singleton-Methoden für Objekte zu definieren. Diese Methoden werden als neue Instanzmethoden zur speziellen, versteckten Singleton-Klasse des Objekts hinzugefügt :

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]

Aber sind Klassen und Module nicht auch nur einfache Objekte? In der Tat sind sie! Bedeutet das, dass sie auch Singleton-Methoden haben können? Ja tut es! Und so entstehen Klassenmethoden:

class Abc
end

# define singleton method
def Abc.foo
  "foo"
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Die üblichere Methode zum Definieren einer Klassenmethode ist die Verwendung selfinnerhalb des Klassendefinitionsblocks, der sich auf das zu erstellende Klassenobjekt bezieht:

class Abc
  def self.foo
    "foo"
  end
end

Abc.singleton_class.instance_methods(false)
#=> [:foo]

Wie füge ich die Klassenmethoden in ein Modul ein?

Wie wir gerade festgestellt haben, sind Klassenmethoden wirklich nur Instanzmethoden für die Singleton-Klasse des Klassenobjekts. Bedeutet dies, dass wir nur ein Modul in die Singleton-Klasse aufnehmen können , um eine Reihe von Klassenmethoden hinzuzufügen? Ja tut es!

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"

Diese self.singleton_class.include M::ClassMethodsZeile sieht nicht sehr gut aus, also fügte Ruby hinzu Object#extend, was dasselbe tut - dh ein Modul in die Singleton-Klasse des Objekts einschließt:

class HostKlass
  include M
  extend M::ClassMethods
end

HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
#    ^ there it is!

Bewegen des extendAnrufs in das Modul

Dieses vorherige Beispiel ist aus zwei Gründen kein gut strukturierter Code:

  1. Wir müssen jetzt beide include und extendin der HostClassDefinition aufrufen , damit unser Modul richtig aufgenommen wird. Dies kann sehr umständlich werden, wenn Sie viele ähnliche Module einbinden müssen.
  2. HostClassVerweise direkt M::ClassMethods, dies ist ein Implementierungsdetail des Moduls M, HostClassdas nicht bekannt sein oder sich darum kümmern sollte.

Wie wäre es damit: Wenn wir includein der ersten Zeile aufrufen , benachrichtigen wir das Modul irgendwie darüber, dass es enthalten ist, und geben ihm auch unser Klassenobjekt, damit es sich extendselbst aufrufen kann. Auf diese Weise ist es Aufgabe des Moduls, die Klassenmethoden hinzuzufügen, wenn dies gewünscht wird.

Genau dafür ist die spezielle self.includedMethode gedacht. Ruby ruft diese Methode automatisch auf, wenn das Modul in einer anderen Klasse (oder einem anderen Modul) enthalten ist, und übergibt das Hostklassenobjekt als erstes 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!

Natürlich ist das Hinzufügen von Klassenmethoden nicht das einzige, was wir tun können self.included. Wir haben das Klassenobjekt, also können wir jede andere (Klassen-) Methode darauf aufrufen:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Máté Solymosi
quelle
2
Wunderbare Antwort! Konnte das Konzept nach einem Tag des Kampfes endlich verstehen. Danke dir.
Sankalp
1
Ich denke, dies könnte die beste schriftliche Antwort sein, die ich je auf SO gesehen habe. Vielen Dank für die unglaubliche Klarheit und für die Erweiterung meines Verständnisses von Ruby. Wenn ich dies einen 100pt Bonus schenken könnte, würde ich!
Peter Nixey
7

Wie Sergio in den Kommentaren erwähnt hat, ist es für Leute, die bereits in Rails sind (oder nichts dagegen haben, abhängig vom aktiven Support ), Concernhier hilfreich:

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
Franklin Yu
quelle
3

Sie können Ihren Kuchen haben und ihn auch essen, indem Sie dies tun:

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"]

Wenn Sie beabsichtigen, Instanz- und Klassenvariablen hinzuzufügen, werden Sie sich am Ende die Haare ausreißen, da Sie auf eine Menge fehlerhaften Codes stoßen, es sei denn, Sie tun dies auf diese Weise.

Bryan Colvin
quelle
Es gibt einige seltsame Dinge, die beim Übergeben eines Blocks an class_eval nicht funktionieren, z. B. das Definieren von Konstanten, das Definieren verschachtelter Klassen und die Verwendung von Klassenvariablen außerhalb von Methoden. Um diese Dinge zu unterstützen, können Sie class_eval anstelle eines Blocks einen Heredoc (String) geben: base.class_eval << - 'END'
Paul Donohue