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 ancestors
metodenya:
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 M
ke C
, M
sekarang adalah leluhur dari C
, jadi ketika kita memanggil foo
sebuah 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 Class
dan 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 self
blok 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::ClassMethods
baris 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:
- Sekarang kita harus memanggil keduanya
include
dan extend
dalam HostClass
definisi untuk memasukkan modul kita dengan benar. Ini bisa menjadi sangat rumit jika Anda harus menyertakan banyak modul serupa.
HostClass
referensi langsung M::ClassMethods
, yang merupakan detail implementasi dari modul M
yang HostClass
seharusnya tidak perlu diketahui atau dipedulikan.
Jadi bagaimana dengan ini: ketika kita memanggil include
baris pertama, kita entah bagaimana memberi tahu modul bahwa itu telah disertakan, dan juga memberikannya objek kelas kita, sehingga itu bisa memanggil extend
dirinya 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