Dziedziczenie metod klas z modułów / miksów w Rubim

95

Wiadomo, że w Rubim dziedziczone są metody klasowe:

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

Jednak dziwi mnie, że nie działa z miksami:

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

Wiem, że metoda #extend może to zrobić:

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

Ale piszę mixin (a raczej chciałbym napisać) zawierający zarówno metody instancji, jak i metody klas:

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

Teraz chciałbym zrobić to:

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

Chcę, aby A, B dziedziczyły zarówno metody instancji, jak i klasy z Commonmodułu. Ale to oczywiście nie działa. Czy nie istnieje więc sekretny sposób, aby to dziedziczenie działało z pojedynczego modułu?

Wydaje mi się nieeleganckie, aby podzielić to na dwa różne moduły, jeden do uwzględnienia, a drugi do rozszerzenia. Innym możliwym rozwiązaniem byłoby użycie klasy Commonzamiast modułu. Ale to tylko obejście. (A co, jeśli istnieją dwa zestawy wspólnych funkcji Common1i Common2naprawdę musimy mieć mixiny?) Czy jest jakiś głęboki powód, dla którego dziedziczenie metod klas nie działa z miksów?

Boris Stitnicky
źródło
1
Z tym wyróżnieniem, że tutaj wiem, że jest to możliwe - proszę o jak najmniej brzydki sposób zrobienia tego oraz z powodów, dla których naiwny wybór nie działa.
Boris Stitnicky
1
Z większym doświadczeniem zrozumiałem, że Ruby posunąłby się za daleko, odgadując zamiary programisty, gdyby dołączenie modułu dodało również metody modułu do pojedynczej klasy elementu włączającego. Dzieje się tak, ponieważ „metody modułowe” są w rzeczywistości niczym innym jak metodami pojedynczymi. Moduły nie są specjalne, jeśli chodzi o metody singleton, są specjalne, ponieważ są przestrzeniami nazw, w których zdefiniowane są metody i stałe. Przestrzeń nazw jest całkowicie niezwiązana z pojedynczymi metodami modułu, więc w rzeczywistości dziedziczenie klas metod singletonowych jest bardziej zadziwiające niż jego brak w modułach.
Boris Stitnicky,

Odpowiedzi:

171

Popularnym idiomem jest użycie includedstamtąd metod klasy przechwytującej i wstrzykującej.

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
źródło
26
includedodaje metody instancji, extenddodaje metody klas. Tak to działa. Nie widzę niespójności, tylko niespełnione oczekiwania :)
Sergio Tulentsev
1
Powoli godzę się na fakt, że Pańska sugestia jest tak elegancka, jak praktyczne rozwiązanie tego problemu. Ale byłbym wdzięczny za poznanie powodu, dla którego coś, co działa z klasami, nie działa z modułami.
Boris Stitnicky
6
@BorisStitnicky Zaufaj tej odpowiedzi. Jest to bardzo popularny idiom w języku Ruby, rozwiązujący dokładnie przypadek użycia, o który pytasz, i dokładnie z powodów, których doświadczyłeś. Może wyglądać „nieelegancko”, ale to najlepszy wybór. (Jeśli robisz to często, możesz przenieść includeddefinicję metody do innego modułu i uwzględnić TO w swoim głównym module;)
Phrogz
2
Przeczytaj ten wątek, aby uzyskać więcej informacji na temat „dlaczego?” .
Phrogz
2
@werkshy: dołącz moduł do atrapy klasy.
Sergio Tulentsev,
47

Oto cała historia, wyjaśniająca niezbędne koncepcje metaprogramowania potrzebne do zrozumienia, dlaczego włączanie modułów działa tak, jak w Rubim.

Co się dzieje, gdy dołączony jest moduł?

Dołączenie modułu do klasy powoduje dodanie modułu do przodków klasy. Możesz spojrzeć na przodków dowolnej klasy lub modułu, wywołując jego ancestorsmetodę:

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!

Kiedy wywołujesz metodę na instancji programu C, Ruby spojrzy na każdy element tej listy przodków w celu znalezienia metody instancji o podanej nazwie. Ponieważ włączyliśmy Mdo C, Mjest teraz przodkiem C, więc kiedy wywołasz foowystąpienie C, Ruby znajdzie tę metodę w M:

C.new.foo
#=> "foo"

Zwróć uwagę, że dołączenie nie kopiuje żadnej instancji ani metod klas do klasy - po prostu dodaje "uwagę" do klasy, że powinna ona również szukać metod instancji w dołączonym module.

A co z metodami „klasowymi” w naszym module?

Ponieważ włączanie zmienia tylko sposób wysyłania metod instancji, dołączenie modułu do klasy powoduje, że jego metody instancji są dostępne tylko w tej klasie. Metody "klasy" i inne deklaracje w module nie są automatycznie kopiowane do klasy:

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

W jaki sposób Ruby implementuje metody klas?

W Rubim klasy i moduły są zwykłymi obiektami - są instancjami klasy Classi Module. Oznacza to, że możesz dynamicznie tworzyć nowe klasy, przypisywać je do zmiennych itp .:

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

klass.new.foo
#=> "foo"

Również w Rubim masz możliwość definiowania tak zwanych metod singletonowych na obiektach. Te metody są dodawane jako nowe metody instancji do specjalnej, ukrytej klasy pojedynczej obiektu:

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]

Ale czy klasy i moduły nie są również zwykłymi obiektami? W rzeczywistości są! Czy to oznacza, że ​​mogą też mieć metody singletonowe? Tak! I tak rodzą się metody klasowe:

class Abc
end

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

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

Lub bardziej powszechnym sposobem definiowania metody klasy jest użycie selfw bloku definicji klasy, który odnosi się do tworzonego obiektu klasy:

class Abc
  def self.foo
    "foo"
  end
end

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

Jak dołączyć metody klasy do modułu?

Jak już ustaliliśmy, metody klas są w rzeczywistości tylko metodami instancji klasy pojedynczej obiektu klasy. Czy to oznacza, że ​​możemy po prostu dołączyć moduł do klasy pojedynczej, aby dodać kilka metod klasowych? Tak!

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"

Ta self.singleton_class.include M::ClassMethodslinia nie wygląda zbyt ładnie, więc dodał Ruby Object#extend, który robi to samo - tj. Włącza moduł do pojedynczej klasy obiektu:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Przenoszenie extend wywołania do modułu

Ten poprzedni przykład nie jest dobrze zorganizowany z dwóch powodów:

  1. Musimy teraz wywołać oba elementy include oraz extendw HostClassdefinicji, aby poprawnie uwzględnić nasz moduł. Może to być bardzo kłopotliwe, jeśli musisz dołączyć wiele podobnych modułów.
  2. HostClassbezpośrednie odniesienia M::ClassMethods, które są szczegółem implementacyjnym modułu M, HostClasso którym nie trzeba wiedzieć ani o nim dbać.

A co powiesz na to: kiedy wywołujemy includepierwszą linię, w jakiś sposób powiadamiamy moduł, że został dołączony, a także nadajemy mu nasz obiekt klasy, aby mógł się wywołać extend. W ten sposób zadaniem modułu jest dodanie metod klasy, jeśli chce.

Właśnie do tego służy ta specjalna self.includedmetoda . Ruby automatycznie wywołuje tę metodę za każdym razem, gdy moduł jest włączony do innej klasy (lub modułu) i przekazuje obiekt klasy hosta jako pierwszy 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!

Oczywiście dodawanie metod klasowych nie jest jedyną rzeczą, którą możemy zrobić self.included. Mamy obiekt klasy, więc możemy wywołać na nim dowolną inną metodę (klasę):

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Máté Solymosi
źródło
2
Wspaniała odpowiedź! W końcu udało mi się zrozumieć koncepcję po całym dniu walki. Dziękuję Ci.
Sankalp
1
Myślę, że to może być najlepiej napisana odpowiedź, jaką kiedykolwiek widziałem w SO. Dziękuję za niesamowitą przejrzystość i rozszerzenie mojego zrozumienia Rubiego. Gdybym mógł podarować temu bonus 100 punktów, zrobiłbym to!
Peter Nixey
7

Jak Sergio wspomniał w komentarzach, dla facetów, którzy są już w Railsach (lub nie mają nic przeciwko, w zależności od Active Support ), Concernjest tutaj pomocny:

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
źródło
3

Możesz wziąć ciasto i zjeść je, wykonując następujące czynności:

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

Jeśli zamierzasz dodać instancje i zmienne klasowe, skończysz z wyrywaniem swoich włosów, ponieważ napotkasz mnóstwo zepsutego kodu, chyba że zrobisz to w ten sposób.

Bryan Colvin
źródło
Jest kilka dziwnych rzeczy, które nie działają podczas przekazywania class_eval bloku, takich jak definiowanie stałych, definiowanie klas zagnieżdżonych i używanie zmiennych klasowych poza metodami. Aby wesprzeć te rzeczy, możesz nadać class_eval heredoc (ciąg) zamiast bloku: base.class_eval << - 'END'
Paul Donohue