ExpandoMetaClass

Menambah dan mengganti metode saat runtime menggunakan ExpandoMetaClass.

Daftar isi

Topik ini membahas ExpandoMetaClass, yaitu metakelas milik Groovy yang dapat dikembangkan saat program berjalan untuk menambah atau mengganti metode dan properti. Pembahasan mencakup penambahan metode pada satu objek, penambahan dan penggantian metode pada kelas termasuk kelas milik JDK, penambahan metode statis, serta batas hidup dan biaya performa dari seluruh modifikasi ini. Setiap langkah didemonstrasikan dengan berkas yang dijalankan nyata beserta keluaran aslinya, sehingga perilaku metakelas dapat diperiksa ulang langsung di terminal.

Properti metaClass dan operator penambahannya#

Setiap objek dan kelas Groovy memiliki properti metaClass, yaitu pintu masuk ke metakelas yang menengahi panggilan pada objek tersebut. Melalui pintu inilah ExpandoMetaClass bekerja sebagai implementasi metakelas yang menerima metode baru berbentuk closure. Tanda << menambahkan metode baru, dan penambahan dengan nama yang sama menghasilkan versi metode lain dengan parameter berbeda. Tanda = mengganti implementasi yang sudah ada, termasuk implementasi milik kelas lain.

Karena setiap panggilan selalu melewati metakelas pada protokol metaobjek, metode yang ditempelkan lewat jalur ini tidak terlihat berbeda dari metode yang ditulis di dalam kelas. Bedanya, penempelan dilakukan tanpa menyunting kode aslinya. Teknik mengubah perilaku kelas pihak ketiga saat program berjalan dikenal sebagai monkey patching, dan ExpandoMetaClass adalah alat resmi Groovy untuk keperluan tersebut. Kegunaannya yang paling umum adalah menambah metode pembantu pada tipe bawaan dan mengisolasi perilaku saat pengujian.

Menambah metode pada satu objek#

Penambahan metode pada satu objek berguna ketika perilaku tambahan hanya relevan untuk satu instans, misalnya satu objek sementara dalam sebuah prosedur. Berkas berikut menempelkan metode duaKali pada objek p1 semata. Closure-nya membaca properti nilai secara langsung karena dieksekusi dalam konteks objek pemanggilnya, lalu metode yang sama dipanggil pada objek p2 untuk menunjukkan batas cakupannya. Pola ini menghindarkan penulisan subkelas baru hanya untuk satu perilaku tambahan pada satu tempat.

groovy emc-objek.groovy
// tidak-dijalankan
class Penghitung {
    int nilai = 0
}

def p1 = new Penghitung(nilai: 10)
def p2 = new Penghitung(nilai: 3)

// metode ditambahkan hanya pada objek p1
p1.metaClass.duaKali << { -> nilai * 2 }

println p1.duaKali()
println p1.nilai

// p2 tidak ikut mendapatkan metode tersebut
try {
    p2.duaKali()
} catch (groovy.lang.MissingMethodException e) {
    println "p2 tidak punya metode ${e.method}"
}
bash
# menjalankan demo metakelas satu objek
groovy emc-objek.groovy
text Keluaran
20
10
p2 tidak punya metode duaKali

Keluaran pertama berupa 20 karena closure mengembalikan nilai * 2 dari objek p1, sedangkan keluaran kedua sebesar 10 dan membuktikan bahwa metode itu tidak mengubah nilai aslinya. Baris ketiga menunjukkan bahwa objek p2 dari kelas yang sama tidak mewarisi metode tersebut, dan panggilannya berujung pada MissingMethodException yang ditangkap blok try. Batas cakupan ini adalah keuntungan sekaligus tanggung jawab: perilaku tambahan tidak bocor ke objek lain, tetapi penempatan metode pada banyak objek yang terpisah menjadi pekerjaan yang berulang. Apabila perilaku yang sama dibutuhkan di banyak tempat, penempelan pada metakelas tingkat kelas lebih tepat.

Menambah dan mengganti metode kelas milik JDK#

Penambahan metode pada tingkat kelas membuat seluruh instans mendapatkannya, termasuk instans dari kelas yang ditulis orang lain maupun kelas milik JDK. Berkas berikut memuat dua operasi. Pertama, metode selesai ditambahkan pada kelas Tugas buatan sendiri, dan dua objek berbeda memanggilnya dengan hasil yang mengikuti properti masing-masing. Kedua, metode toUpperCase milik String diganti dengan implementasi baru melalui tanda =. Penggantian pada kelas milik JDK adalah bentuk monkey patching yang paling luas dampaknya, karena setiap teks dalam program ikut terkena.

groovy emc-kelas.groovy
// tidak-dijalankan
// menambah metode pada kelas: semua instans mendapatkannya
class Tugas {
    String nama
}

Tugas.metaClass.selesai << { -> "${nama} selesai" }
println new Tugas(nama: 'menyapu lantai').selesai()
println new Tugas(nama: 'menyiram tanaman').selesai()

// mengganti metode milik kelas JDK memakai tanda sama dengan
String.metaClass.toUpperCase = { -> delegate.toLowerCase().capitalize() }
println 'BELAJAR GROOVY'.toUpperCase()
println 'aPa ITu mOp'.toUpperCase()
bash
# menjalankan demo metakelas tingkat kelas
groovy emc-kelas.groovy
text Keluaran
menyapu lantai selesai
menyiram tanaman selesai
Belajar groovy
Apa itu mop

Dua baris pertama membuktikan bahwa metode selesai berlaku untuk seluruh instans Tugas dan membaca properti nama dari objek pemanggilnya. Pada penggantian metode String, closure dieksekusi dengan delegate berupa objek yang memanggil metode tersebut, yaitu instans String yang bersangkutan, sehingga delegate.toLowerCase() diterapkan pada teks pemanggil. Dua baris terakhir menegaskan penggantian itu berlaku untuk seluruh String dalam program, karena panggilan toUpperCase() yang lazimnya menghasilkan huruf kapital di seluruh teks justru menghasilkan huruf awal kapital dengan sisanya kecil. Perubahan semacam ini memindahkan makna metode bawaan, sehingga kode lain yang mengandalkan perilaku aslinya dapat berperilaku keliru tanpa pesan error apa pun. Penggantian pada tipe bawaan sebaiknya dibatasi untuk kebutuhan seperti pengujian.

Menambah metode statis#

Metode statis ditambahkan melalui jalur metaClass.static, sehingga metode baru melekat pada kelasnya dan bukan pada instansnya. Pola ini sering dipakai untuk metode pabrik (factory), yaitu metode pembuat objek bernama yang lebih mudah dibaca daripada konstruktor. Berkas berikut menambahkan metode kuadrat pada kelas Integer sebagai metode statis, lalu memanggilnya dua kali untuk menunjukkan bahwa metode itu menerima argumen sebagaimana metode statis biasa.

groovy emc-statis.groovy
// tidak-dijalankan
// metode statis ditambahkan melalui metaClass.static
Integer.metaClass.static.kuadrat << { int x -> x * x }

println Integer.kuadrat(9)
println Integer.kuadrat(12)
bash
# menjalankan demo metode statis
groovy emc-statis.groovy
text Keluaran
81
144

Pemanggilan Integer.kuadrat(9) terjadi langsung pada kelas, tanpa instans apa pun, dan hasilnya 81 serta 144 sesuai perhitungan closure-nya. Metode yang ditambahkan pada jalur statis dan jalur instans tidak saling mengganggu, karena keduanya terdaftar pada bagian metakelas yang berbeda. Penambahan pada jalur statis juga berlaku untuk seluruh program, sama seperti penambahan pada metakelas kelas. Metode pabrik semacam ini membuat maksud pemanggilan terbaca sebagai sebuah operasi bernama, misalnya Durasi.dariMenit(90), alih-alih angka mentah yang diteruskan ke konstruktor. Perlu diingat bahwa pemanggilannya tetap disalurkan secara dinamis, sehingga biaya pencarian metakelas tetap ada pada setiap panggilannya.

Batas hidup dan biaya performa#

Modifikasi melalui ExpandoMetaClass bersifat sementara dan berada pada ruang lingkup yang sempit. Seluruh perubahan hanya hidup di dalam proses yang menjalankannya, dan hanya pada classloader yang sama, yaitu komponen runtime yang memuat definisi kelas ke memori. Begitu program berakhir, tidak ada berkas maupun kelas yang berubah secara permanen.

Dari sisi performa, setiap panggilan yang melewati metakelas membawa biaya pencarian tambahan dibandingkan panggilan metode yang diselesaikan langsung oleh kompilator. Biaya ini wajar untuk skrip dan kode administratif, tetapi menumpuk pada bagian program yang dieksekusi dengan frekuensi tinggi. Pertimbangan tersebut menjadi jembatan ke arah kompilasi statis, karena pemeriksaan tipe saat kompilasi membutuhkan panggilan yang tertata tanpa penempelan runtime. Kode yang menuntut keandalan dan kecepatan sebaiknya menghindari ketergantungan pada metode yang hanya ada lewat metakelas.

Ringkasan#

  • Properti metaClass tersedia pada objek maupun kelas, dengan tanda << untuk menambahkan metode dan tanda = untuk menggantinya.
  • Metode yang ditempelkan pada metakelas satu objek hanya berlaku untuk objek itu, sedangkan metakelas kelas memengaruhi seluruh instansnya.
  • Metode milik JDK dapat ditambahkan atau diganti, dan closure-nya dieksekusi dengan delegate berupa objek pemanggil.
  • Seluruh modifikasi hanya hidup pada runtime dan classloader yang sama, dengan biaya pencarian tambahan pada setiap panggilan dinamis.

Referensi#