Pengenalan metaprogramming

Menulis kode yang menghasilkan kode dengan define_method dan mengenal method_missing.

Daftar isi

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.

ruby aksesoris.rb
# 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.mode
text Keluaran
localhost
9090
aman

Badan 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.

ruby laporan_method_missing.rb
# 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
end
text Keluaran
12
true
false
NoMethodError
undefined method 'tidak_ada' for an instance of Laporan

Pemanggilan 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.

ruby risiko_tanpa_super.rb
# 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
end
text Keluaran
SystemStackError
stack level too deep

Panggilan 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_method mendefinisikan metode saat program berjalan, sehingga kumpulan aksesor yang berpola sama dapat dihasilkan dari satu perulangan.
  • method_missing menangkap panggilan metode yang tidak ditemukan, dengan super pada cabang terakhir agar error yang tidak tertangani tidak tersamarkan.
  • respond_to_missing? menjaga jawaban respond_to? tetap benar atas metode dinamis yang ditangani method_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#