Modul di Ruby juga berperan sebagai wadah metode yang dapat dilekatkan ke kelas mana pun, dan mekanisme ini dikenal sebagai mixin. Topik ini membahas cara kerja include yang mengubah metode modul menjadi metode instans, extend yang menempelkan metode modul pada satu objek, pembacaan jalur pencarian metode lewat ancestors, serta pertimbangan memilih mixin atau pewarisan. Setelah membacanya, pembagian perilaku antarkelas dapat dirancang tanpa menumpuk hierarki kelas.
include: metode modul menjadi metode instans#
Metode yang didefinisikan di dalam modul menjadi milik instans pada setiap kelas yang menyertakannya lewat include. Kelas yang berbeda dapat menyertakan modul yang sama, sehingga satu definisi perilaku digunakan di banyak tempat tanpa saling mewarisi. Contoh berikut menempatkan metode deskripsi di dalam modul Deskriptif, lalu kelas Buku menyertakannya.
# Modul Deskriptif berbagi metode deskripsi kepada kelas mana pun yang menyertakannya.
module Deskriptif
def deskripsi
"#{judul} karya #{penulis}"
end
end
class Buku
include Deskriptif
attr_reader :judul, :penulis
def initialize(judul, penulis)
@judul = judul
@penulis = penulis
end
end
buku = Buku.new("Kamus Kecil", "Dewi")
puts buku.deskripsi
puts buku.is_a?(Deskriptif)Kamus Kecil karya Dewi
trueMetode deskripsi berjalan mulus padahal badan metode itu membaca judul serta penulis yang tidak didefinisikan di dalam modul. Hal ini terjadi karena pemanggilan metode dijalankan pada objek penerima, sehingga metode pembaca judul dan penulis milik Buku tetap terjangkau dari badan modul. Metode is_a? pada baris terakhir mengonfirmasi keanggotaan itu, yaitu objek Buku juga tergolong instans dari modul Deskriptif. Pola seperti inilah yang menjadikan modul tempat berbagi kemampuan, dan bukan sekadar wadah konstanta seperti pada pembahasan namespace.
ancestors dan urutan pencarian metode#
Setiap pemanggilan metode disusuri menurut urutan tetap yang disebut method lookup, dan daftar lengkapnya dapat dibaca lewat ancestors. Pada kasus yang melibatkan mixin, Ruby memeriksa kelas milik objek terlebih dahulu, kemudian modul yang disertakan oleh kelas tersebut, lalu naik ke kelas induk beserta modul-modul yang disertakan kelas induk itu, sampai definisi ditemukan. Diagram berikut memperlihatkan jalur tersebut untuk kelas Suhu yang menyertakan DapatDihitung.
Program berikut menempatkan metode hitung di modul DapatDihitung, lalu membandingkan kelas yang menyertakan modul dengan subkelas yang mendefinisikan metode bernama sama.
# Urutan pencarian metode: kelas terlebih dahulu, lalu modul yang disertakan.
module DapatDihitung
def hitung
:dari_modul
end
end
class Suhu
include DapatDihitung
end
class SuhuKalibrasi < Suhu
def hitung
:dari_kelas
end
end
puts Suhu.new.hitung
puts SuhuKalibrasi.new.hitung
p Suhu.ancestors
p SuhuKalibrasi.ancestorsdari_modul
dari_kelas
[Suhu, DapatDihitung, Object, Kernel, BasicObject]
[SuhuKalibrasi, Suhu, DapatDihitung, Object, Kernel, BasicObject]Pemanggilan pertama menemukan hitung di dalam modul karena kelas Suhu tidak mendefinisikannya, sedangkan pemanggilan kedua menemukan versi kelas lebih dahulu sehingga menghasilkan :dari_kelas. Daftar ancestors menjelaskan kedua hasil itu secara persis, yaitu Suhu mendahului DapatDihitung pada daftar pertama, sedangkan pada daftar kedua SuhuKalibrasi dan Suhu mendahului DapatDihitung milik Suhu. Pemeriksaan pun bergerak dari kiri ke kanan, sehingga metode milik kelas selalu diperiksa lebih dahulu daripada metode milik modul yang menyertainya.
Metode modul juga dapat meneruskan pemanggilan dengan super, seolah-olah posisi modul berada di tengah rantai pencarian. Program berikut menyertakan DapatDihitung pada subkelas, lalu metode hitung dari modul meneruskan pemanggilan ke versi kelas induk lewat super.
# Metode modul dapat memanggil super untuk meneruskan ke rantai pencarian.
module DapatDihitung
def hitung
"modul + #{super}"
end
end
class Suhu
def hitung
"hitungan dasar"
end
end
class SuhuKalibrasi < Suhu
include DapatDihitung
end
puts SuhuKalibrasi.new.hitung
p SuhuKalibrasi.ancestorsmodul + hitungan dasar
[SuhuKalibrasi, DapatDihitung, Suhu, Object, Kernel, BasicObject]Keluarannya berupa gabungan dua tahap, yaitu tambahan dari modul diikuti hitungan dasar dari kelas Suhu. Daftar ancestors pada baris terakhir menunjukkan DapatDihitung terjepit di antara keduanya, dan super cukup mengikuti daftar itu ke kanan. Susunan tersebut memperlihatkan aturan umumnya, yaitu kelas dahulu, kemudian mixin milik kelas itu, baru induk beserta modul-modul induknya. Dengan mekanisme ini, mixin aman digunakan untuk membungkus perilaku di sekitar metode yang sudah ada tanpa menyentuh definisi aslinya.
extend: metode modul menjadi metode kelas#
extend melekatkan metode modul pada satu objek tertentu, dan apabila objek itu sebuah kelas, metode tersebut menjadi metode kelas. Perbedaan utama dengan include terletak pada penerimanya, yaitu include bekerja pada kelas dan menggolongkan metode ke sisi instans, sedangkan extend bekerja pada objek langsung dan menempelkan metodenya pada sisi kelas dari objek tersebut.
# extend menempelkan metode modul sebagai metode milik objek penerimanya.
module EksporCsv
def ekspor_csv(daftar)
daftar.join(",")
end
end
class Sensor
extend EksporCsv
end
puts Sensor.ekspor_csv([20, 21, 22])
begin
Sensor.new.ekspor_csv([20])
rescue NoMethodError => e
puts e.class
puts e.message
end
satuan = Sensor.new
satuan.extend(EksporCsv)
puts satuan.ekspor_csv([1, 2, 3])20,21,22
NoMethodError
undefined method 'ekspor_csv' for an instance of Sensor
1,2,3Pemanggilan Sensor.ekspor_csv berhasil karena extend di dalam badan kelas menempelkan metode itu pada kelas Sensor. Pemanggilan kedua gagal dan menunjukkan sisi lainnya, yaitu instans Sensor tidak menerima metode tersebut, karena extend pada kelas tidak memasukkan apa pun ke rantai pencarian metode instans. Pemanggilan terakhir memperlihatkan bentuk umumnya, yaitu extend dapat dipanggil pada objek mana pun bahkan setelah objek dibuat, sehingga satu instans tertentu memperoleh kemampuan tambahan tanpa mengubah instans lainnya.
Mixin atau pewarisan#
Pewarisan menggambarkan hubungan sejenis, yaitu SuhuKalibrasi memang sejenis Suhu, sedangkan mixin menggambarkan hubungan memiliki kemampuan, sehingga kelas mana pun dapat mendeskripsikan dirinya lewat modul Deskriptif. Aturan praktisnya, pewarisan dipilih apabila dua kelas memang sejenis dan berbagi keadaan, sedangkan mixin dipilih apabila kemampuannya berdiri sendiri dan layak dilekatkan ke banyak kelas yang tidak saling berhubungan. Sebuah kelas juga dapat menyertakan lebih dari satu modul, sedangkan Ruby hanya mengizinkan satu kelas induk, sehingga mixin menyelesaikan kebutuhan berbagi perilaku dari beberapa arah sekaligus.
| Kebutuhan | Pilihan yang tepat |
|---|---|
| Hubungan sejenis antara dua kelas | pewarisan dengan < |
| Perilaku tambahan untuk instans dari banyak kelas yang tidak saling berhubungan | include pada modul bersama |
| Perilaku tambahan pada sisi kelas atau pada satu objek tertentu | extend |
Ketiga pilihan itu saling melengkapi dan sering digunakan bersama dalam satu kelas, sebagaimana daftar ancestors pada contoh di atas yang memuat kelas, mixin, dan leluhur bawaan sekaligus. Pustaka standar Ruby juga dibangun dengan pola yang sama, dan penerapannya yang paling terkenal adalah modul Comparable dan Enumerable.
Kesalahan umum#
Kesalahan yang sering terjadi adalah menganggap seluruh metode modul ikut pindah lewat include, padahal metode yang didefinisikan dengan def self. tetap menempel pada modulnya. Program berikut mendefinisikan metode rata_rata dengan def self. di modul Statistik, lalu kelas NilaiUjian menyertakan modul tersebut dan pemanggilan dari instans gagal.
# Metode berawalan def self. tidak ikut saat modul disertakan lewat include.
module Statistik
def self.rata_rata(daftar)
daftar.sum / daftar.length
end
end
class NilaiUjian
include Statistik
end
begin
NilaiUjian.new.rata_rata([80, 90])
rescue NoMethodError => e
puts e.class
puts e.message
end
puts Statistik.rata_rata([80, 90])NoMethodError
undefined method 'rata_rata' for an instance of NilaiUjian
85Metode rata_rata tetap dapat dipanggil langsung pada modulnya, sebagaimana baris terakhir program, karena def self. memang mendefinisikan metode milik objek Statistik sendiri. Perbaikannya mengikuti maksud definisi, yaitu menggunakan def biasa di dalam modul apabila metode ditujukan untuk instans lewat include, atau menggunakan extend apabila metode dimaksudkan sebagai metode kelas. Membedakan keduanya sejak awal menjaga modul tetap netral terhadap cara penyertaannya.
Ringkasan#
includemenyertakan modul ke dalam kelas sehingga metode modul menjadi metode instans yang dapat membaca keadaan objek.extendmenempelkan metode modul pada satu objek, dan hasilnya menjadi metode kelas apabila penerimanya sebuah kelas.ancestorsmenampilkan urutan pencarian metode, yaitu kelas dahulu, lalu mixin, kemudian induk beserta mixin induknya, sampai BasicObject.- Metode dengan
def self.tidak berpindah lewatinclude, dan mixin dipilih apabila kemampuan berdiri sendiri, sedangkan pewarisan dipilih apabila kedua kelas memang sejenis.
Referensi#
- Module — Ruby
— Dokumentasi
include,ancestors, dan konstanta modul pada kelas Module. - Object#extend — Ruby — Dokumentasi extend beserta penjelasan penempelannya pada satu objek.