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 Common
moduł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 Common
zamiast modułu. Ale to tylko obejście. (A co, jeśli istnieją dwa zestawy wspólnych funkcji Common1
i Common2
naprawdę musimy mieć mixiny?) Czy jest jakiś głęboki powód, dla którego dziedziczenie metod klas nie działa z miksów?
Odpowiedzi:
Popularnym idiomem jest użycie
included
stamtą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"
źródło
include
dodaje metody instancji,extend
dodaje metody klas. Tak to działa. Nie widzę niespójności, tylko niespełnione oczekiwania :)included
definicję metody do innego modułu i uwzględnić TO w swoim głównym module;)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
ancestors
metodę: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śmyM
doC
,M
jest teraz przodkiemC
, więc kiedy wywołaszfoo
wystąpienieC
, Ruby znajdzie tę metodę wM
: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
Class
iModule
. 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
self
w 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::ClassMethods
linia nie wygląda zbyt ładnie, więc dodał RubyObject#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łuTen poprzedni przykład nie jest dobrze zorganizowany z dwóch powodów:
include
orazextend
wHostClass
definicji, aby poprawnie uwzględnić nasz moduł. Może to być bardzo kłopotliwe, jeśli musisz dołączyć wiele podobnych modułów.HostClass
bezpośrednie odniesieniaM::ClassMethods
, które są szczegółem implementacyjnym modułuM
,HostClass
o którym nie trzeba wiedzieć ani o nim dbać.A co powiesz na to: kiedy wywołujemy
include
pierwszą 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.included
metoda . 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
źródło
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 ),
Concern
jest 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
źródło
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.
źródło