Metaprogramming adalah teknik menulis kode yang menghasilkan kode lain saat program berjalan, sehingga tidak semua definisi metode ditulis tangan satu per satu. Topik ini membahas dua mekanisme dasarnya di Ruby, yaitu define_method untuk mendefinisikan metode dari dalam perulangan dan method_missing untuk menangkap panggilan metode yang tidak didefinisikan, beserta respond_to_missing? yang melengkapinya, serta risiko yang menyertai mekanisme tersebut. Mekanisme ini menjelaskan cara kerja banyak pustaka Ruby yang antarmukanya terasa luas meskipun kodenya ringkas.
Metode yang dihasilkan saat program berjalan#
Dalam pendekatan biasa, definisi metode ditulis saat program masih berupa teks, sedangkan define_method mendefinisikannya saat program berjalan. Metode ini menerima nama metode dalam bentuk string atau symbol beserta blok (block) yang menjadi badannya, dan setiap pemanggilan di dalam kelas menambahkan satu metode baru ke kelas tersebut. Karena menerima argumen biasa, nama metode dapat dihitung, dibentuk dari data, atau diambil dari perulangan.
Kebutuhan semacam itu muncul pada kelas dengan aksesor yang berpola sama. Kelas Pengaturan berikut menyimpan tiga pengaturan, dan masing-masing memerlukan getter yang mengembalikan nilai default serta setter yang menimpanya. Menulis enam metode tersebut satu per satu berarti mengulang pola yang sama enam kali, sedangkan versi di bawah menghasilkan semuanya dari satu perulangan.
# define_method mendefinisikan metode saat program berjalan
class Pengaturan
NILAI_DEFAULT = {
"host" => "localhost",
"port" => 8080,
"mode" => "aman"
}.freeze
NILAI_DEFAULT.each_key do |kunci|
define_method(kunci) do
instance_variable_get("@#{kunci}") || NILAI_DEFAULT[kunci]
end
define_method("#{kunci}=") do |nilai|
instance_variable_set("@#{kunci}", nilai)
end
end
end
pengaturan = Pengaturan.new
puts pengaturan.host
pengaturan.port = 9090
puts pengaturan.port
puts pengaturan.modelocalhost
9090
amanBadan kelas menjalankan NILAI_DEFAULT.each_key saat berkas dimuat, dan setiap putaran perulangan memanggil define_method dua kali, sekali untuk getter dan sekali untuk setter. Nama setter dibentuk dengan menambahkan tanda = pada akhir nama, misalnya port=, sehingga pemanggilan pengaturan.port = 9090 menjalankan metode hasil perulangan tersebut. Nilai pengaturan disimpan dalam variabel instans yang namanya diinterpolasi dari kuncinya, dan apabila variabel itu belum diisi, getter mengembalikan nilai default dari hash. Menambahkan pengaturan baru berarti menambahkan satu entri pada NILAI_DEFAULT, dan metode pasangannya lahir tanpa menyentuh bagian lain kelas.
method_missing dan respond_to_missing?#
Setiap panggilan metode di Ruby mencari definisinya sepanjang rantai kelas dan modul milik objeknya. Apabila pencarian berakhir tanpa hasil, Ruby tidak langsung berhenti, melainkan memanggil method_missing dengan nama metode dan argumennya. Definisi bawaan dari metode ini menimbulkan NoMethodError, dan kelas yang menggantinya memperoleh kesempatan menafsirkan panggilan yang tidak pernah didefinisikan.
Kelas Laporan berikut menyimpan datanya dalam hash, lalu menjadikan setiap kunci hash dapat dibaca seolah-olah melalui metode biasa.
# method_missing menangkap panggilan metode yang tidak didefinisikan
class Laporan
def initialize
@data = { "total" => 12, "rata_rata" => 7.5 }
end
def method_missing(nama, *argumen)
if @data.key?(nama.to_s)
@data[nama.to_s]
else
super
end
end
def respond_to_missing?(nama, termasuk_privat = false)
@data.key?(nama.to_s) || super
end
end
laporan = Laporan.new
puts laporan.total
puts laporan.respond_to?(:rata_rata)
puts laporan.respond_to?(:tidak_ada)
begin
laporan.tidak_ada
rescue => e
puts e.class
puts e.message
end12
true
false
NoMethodError
undefined method 'tidak_ada' for an instance of LaporanPemanggilan laporan.total tidak menemukan metode bernama total, sehingga berakhir di method_missing. Parameter nama menerima symbol :total, lalu badannya memeriksa kunci hash dan mengembalikan nilainya. Cabang else memanggil super, yang meneruskan panggilan ke method_missing milik leluhur kelas dan menimbulkan NoMethodError yang wajar. Pemanggilan super pada cabang itu bersifat wajib, karena tanpanya nama metode yang memang tidak ada akan terselubung dan error yang muncul menjadi sulit dilacak. Pada contoh, panggilan laporan.tidak_ada jatuh ke cabang tersebut, ditangkap dengan rescue, dan pesannya tetap jujur menyebut metode yang tidak dikenal.
Metode respond_to_missing? melengkapi pasangan ini. Pemeriksaan respond_to? pada objek yang menimpa method_missing harus tetap jujur, yaitu menyatakan true hanya untuk nama yang benar-benar dapat ditangani. Kelas Laporan mengembalikan true untuk :rata_rata dan false untuk :tidak_ada, sesuai keluaran di atas. Tanpa penimpaan respond_to_missing?, kedua pertanyaan itu dijawab false karena metodenya memang tidak terdefinisi, dan kode yang memeriksa ketersediaan metode sebelum memanggilnya, misalnya kode yang menjaga kompatibilitas dengan objek lain, akan melewatkan kemampuan yang sebenarnya ada.
Kesalahan umum#
Kesalahan yang paling merugikan adalah menimpa method_missing tanpa memanggil super di cabang yang tidak menangani nama. Akibatnya tidak selalu terlihat seketika, tetapi sering berujung rekursi tanpa akhir. Rekursi terjadi ketika badan metode memanggil metode lain yang juga tidak ada, sehingga setiap panggilan yang gagal memanggil method_missing lagi. Berkas berikut memperagakannya dengan kelas yang badan method_missing-nya memanggil simpan, sebuah metode yang tidak pernah didefinisikan.
# method_missing tanpa super dapat berujung rekursi tanpa akhir
class Pencatat
def method_missing(nama, *argumen)
simpan(nama)
end
end
begin
Pencatat.new.catat("harga")
rescue SystemStackError => e
puts e.class
puts e.message
endSystemStackError
stack level too deepPanggilan catat memicu method_missing, yang memanggil simpan, yang juga tidak ada, sehingga method_missing dipanggil lagi dengan nama simpan, dan seterusnya hingga tumpukan pemanggilan habis. Error yang muncul adalah SystemStackError dengan pesan stack level too deep, dan pesan ini pada kode yang menggunakan method_missing hampir selalu menjadi petunjuk rekursi semacam itu. Perbaikannya adalah memanggil super pada cabang yang tidak menangani nama, seperti pada kelas Laporan.
Risiko lain bersifat lebih halus. Panggilan yang melewati method_missing menempuh pencarian yang lebih panjang daripada pemanggilan metode biasa, sehingga pola ini kurang cocok untuk bagian program yang paling sering dieksekusi. Kejelasan juga ikut berkurang, karena metode dinamis tidak terlihat pada daftar metode objek dan tidak terbaca dari definisi kelasnya, sehingga respond_to_missing? serta dokumentasi menjadi penyeimbangnya.
Ringkasan#
define_methodmendefinisikan metode saat program berjalan, sehingga kumpulan aksesor yang berpola sama dapat dihasilkan dari satu perulangan.method_missingmenangkap panggilan metode yang tidak ditemukan, dengansuperpada cabang terakhir agar error yang tidak tertangani tidak tersamarkan.respond_to_missing?menjaga jawabanrespond_to?tetap benar atas metode dinamis yang ditanganimethod_missing.- Pola ini digunakan secara bijak ketika nama metodenya memang dinamis dan seragam, disertai dokumentasi, karena kinerjanya lebih lambat dan metodenya tidak terbaca dari definisi kelas.
Referensi#
- Module#define_method — Dokumentasi Ruby 3.4 — Dokumentasi resmi define_method beserta contoh pembuatan metode saat program berjalan.
- BasicObject#method_missing — Dokumentasi Ruby 3.4 — Dokumentasi resmi method_missing dan perilaku bawaannya saat metode tidak ditemukan.
- Object#respond_to_missing? — Dokumentasi Ruby 3.4 — Dokumentasi resmi respond_to_missing? sebagai pelengkap respond_to? untuk metode dinamis.