Mewarisi metode kelas dari modules / mixins di Ruby

95

Diketahui bahwa di Ruby, metode kelas diturunkan:

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

Namun, saya terkejut karena ini tidak berfungsi dengan mixin:

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

Saya tahu bahwa metode #extend dapat melakukan ini:

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

Tetapi saya sedang menulis sebuah mixin (atau, lebih tepatnya, ingin menulis) yang berisi metode instan dan metode kelas:

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

Sekarang yang ingin saya lakukan adalah:

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

Saya ingin A, B mewarisi metode instance dan class dari Commonmodul. Tapi, tentu saja, itu tidak berhasil. Jadi, bukankah ada cara rahasia untuk membuat warisan ini berfungsi dari satu modul?

Tampaknya tidak enak bagi saya untuk membagi ini menjadi dua modul yang berbeda, satu untuk dimasukkan, yang lain untuk diperluas. Solusi lain yang mungkin adalah menggunakan kelas, Commonbukan modul. Tapi ini hanya solusi. (Bagaimana jika ada dua set fungsi umum Common1dan Common2dan kita benar-benar perlu memiliki mixin?) Apakah ada alasan yang mendalam mengapa pewarisan metode kelas tidak bekerja dari mixin?

Boris Stitnicky
sumber
1
Dengan perbedaan, bahwa di sini, saya tahu itu mungkin - saya meminta cara yang paling jelek untuk melakukannya dan untuk alasan mengapa pilihan yang naif tidak berhasil.
Boris Stitnicky
1
Dengan lebih banyak pengalaman, saya mengerti bahwa Ruby akan bertindak terlalu jauh dengan menebak maksud programmer jika menyertakan modul juga menambahkan metode modul ke kelas tunggal dari includeer. Ini karena "metode modul" sebenarnya tidak lain adalah metode tunggal. Modul tidak khusus untuk memiliki metode tunggal, mereka khusus untuk ruang nama di mana metode dan konstanta didefinisikan. Namespace sama sekali tidak terkait dengan metode tunggal modul, jadi sebenarnya pewarisan kelas metode tunggal lebih mencengangkan daripada kekurangannya dalam modul.
Boris Stitnicky

Jawaban:

171

Idiom umum adalah menggunakan includedmetode kelas hook and inject dari sana.

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
sumber
26
includemenambahkan metode instance, extendmenambahkan metode kelas. Begini Cara kerjanya. Saya tidak melihat ketidakkonsistenan, hanya harapan yang tidak terpenuhi :)
Sergio Tulentsev
1
Saya perlahan-lahan menerima kenyataan, bahwa saran Anda sama elegannya dengan solusi praktis dari masalah ini. Tapi saya akan menghargai mengetahui alasan mengapa sesuatu yang bekerja dengan kelas tidak bekerja dengan modul.
Boris Stitnicky
6
@BorisStitnicky Percayai jawaban ini. Ini adalah ungkapan yang sangat umum di Ruby, memecahkan dengan tepat kasus penggunaan yang Anda tanyakan dan untuk alasan yang Anda alami. Ini mungkin terlihat "tidak elegan", tapi itu taruhan terbaik Anda. (Jika Anda sering melakukan ini, Anda dapat memindahkan includeddefinisi metode ke modul lain dan menyertakan ITU di modul utama Anda;)
Phrogz
2
Baca utas ini untuk wawasan lebih lanjut tentang "mengapa?" .
Phrogz
2
@werkshy: menyertakan modul dalam kelas dummy.
Sergio Tulentsev
47

Berikut adalah kisah lengkapnya, menjelaskan konsep metaprogramming yang diperlukan untuk memahami mengapa penyertaan modul bekerja seperti di Ruby.

Apa yang terjadi jika modul disertakan?

Memasukkan modul ke dalam kelas menambahkan modul ke leluhur kelas. Anda dapat melihat leluhur kelas atau modul apa pun dengan memanggil ancestorsmetodenya:

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!

Saat Anda memanggil metode pada sebuah instance C, Ruby akan melihat setiap item dari daftar leluhur ini untuk menemukan metode instance dengan nama yang diberikan. Karena kita memasukkan Mke C, Msekarang adalah leluhur dari C, jadi ketika kita memanggil foosebuah instance C, Ruby akan menemukan metode itu di M:

C.new.foo
#=> "foo"

Perhatikan bahwa inklusi tidak menyalin setiap contoh atau metode kelas ke kelas - itu hanya menambahkan "catatan" ke kelas yang juga harus mencari metode instan dalam modul yang disertakan.

Bagaimana dengan metode "kelas" dalam modul kita?

Karena inklusi hanya mengubah cara metode instans dikirim, memasukkan modul ke dalam kelas hanya membuat metode instansinya tersedia di kelas itu. Metode "class" dan deklarasi lain dalam modul tidak otomatis disalin ke kelas:

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

Bagaimana Ruby mengimplementasikan metode kelas?

Di Ruby, kelas dan modul adalah objek biasa - mereka adalah instance dari kelas Classdan Module. Ini berarti Anda dapat secara dinamis membuat kelas baru, menugaskannya ke variabel, dll .:

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

klass.new.foo
#=> "foo"

Juga di Ruby, Anda memiliki kemungkinan untuk mendefinisikan apa yang disebut metode tunggal pada objek. Metode ini ditambahkan sebagai metode instance baru ke kelas tunggal khusus yang tersembunyi dari objek:

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]

Tapi bukankah kelas dan modul hanya objek biasa juga? Faktanya mereka! Apakah itu berarti bahwa mereka juga dapat memiliki metode tunggal? Ya, benar! Dan begitulah cara metode kelas lahir:

class Abc
end

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

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

Atau, cara yang lebih umum untuk mendefinisikan metode kelas adalah dengan menggunakan selfblok definisi kelas, yang mengacu pada objek kelas yang sedang dibuat:

class Abc
  def self.foo
    "foo"
  end
end

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

Bagaimana cara menyertakan metode kelas dalam modul?

Seperti yang baru saja kita buat, metode kelas sebenarnya hanyalah metode contoh pada kelas tunggal objek kelas. Apakah ini berarti bahwa kita hanya dapat memasukkan modul ke kelas tunggal untuk menambahkan sekumpulan metode kelas? Ya, benar!

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"

Ini self.singleton_class.include M::ClassMethodsbaris tidak terlihat sangat bagus, sehingga Ruby menambahkan Object#extend, yang melakukan hal yang sama - yaitu termasuk modul ke dalam kelas tunggal dari objek:

class HostKlass
  include M
  extend M::ClassMethods
end

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

Memindahkan extend panggilan ke modul

Contoh sebelumnya ini bukanlah kode yang terstruktur dengan baik, karena dua alasan:

  1. Sekarang kita harus memanggil keduanya include dan extenddalam HostClassdefinisi untuk memasukkan modul kita dengan benar. Ini bisa menjadi sangat rumit jika Anda harus menyertakan banyak modul serupa.
  2. HostClassreferensi langsung M::ClassMethods, yang merupakan detail implementasi dari modul Myang HostClassseharusnya tidak perlu diketahui atau dipedulikan.

Jadi bagaimana dengan ini: ketika kita memanggil includebaris pertama, kita entah bagaimana memberi tahu modul bahwa itu telah disertakan, dan juga memberikannya objek kelas kita, sehingga itu bisa memanggil extenddirinya sendiri. Dengan cara ini, tugas modul adalah menambahkan metode kelas jika diinginkan.

Untuk itulah tepatnya metode khususself.included itu. Ruby secara otomatis memanggil metode ini setiap kali modul dimasukkan ke dalam kelas lain (atau modul), dan meneruskan objek kelas host sebagai argumen pertama:

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!

Tentu saja, menambahkan metode kelas bukanlah satu-satunya hal yang dapat kita lakukan self.included. Kami memiliki objek kelas, jadi kami dapat memanggil metode (kelas) lainnya di atasnya:

def self.included(base)  # `base` is `HostClass` in our case
  base.existing_class_method
  #=> "cool"
end
Máté Solymosi
sumber
2
Jawaban yang luar biasa! Akhirnya bisa memahami konsep setelah seharian berjuang. Terima kasih.
Sankalp
1
Saya pikir ini mungkin jawaban tertulis terbaik yang pernah saya lihat di SO. Terima kasih atas kejelasan yang luar biasa dan untuk memperpanjang pemahaman saya tentang Ruby. Jika saya bisa memberi ini bonus 100pt, saya akan!
Peter Nixey
7

Seperti yang disebutkan Sergio dalam komentar, untuk orang-orang yang sudah menggunakan Rails (atau tidak keberatan bergantung pada Dukungan Aktif ), Concernsangat membantu di sini:

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
sumber
3

Anda bisa mendapatkan kue dan memakannya juga dengan melakukan ini:

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

Jika Anda berniat untuk menambahkan instance, dan variabel kelas, Anda akan berakhir dengan menarik rambut Anda karena Anda akan menemukan banyak kode yang rusak kecuali Anda melakukannya dengan cara ini.

Bryan Colvin
sumber
Ada beberapa hal aneh yang tidak berfungsi saat meneruskan class_eval blok, seperti mendefinisikan konstanta, mendefinisikan kelas bersarang, dan menggunakan variabel kelas di luar metode. Untuk mendukung hal-hal ini, Anda dapat memberikan class_eval sebuah heredoc (string) alih-alih sebuah blok: base.class_eval << - 'END'
Paul Donohue