Setiap closure Groovy membawa tiga properti yang menentukan di mana nama di dalamnya dicari, yaitu owner, delegate, dan thisObject. Topik ini membahas makna ketiganya, kelima strategi resolveStrategy, dan cara kombinasi keduanya menjadi dasar kerja DSL Groovy seperti builder. Setelah membaca topik ini, sebuah closure dapat diarahkan untuk memanggil metode dan properti objek lain secara transparan, tanpa menyebut penerimanya satu pun.
Tiga properti yang menentukan pencarian nama#
Properti owner adalah objek yang melingkupi closure secara langsung, baik kelas maupun closure lain. Properti thisObject merujuk pada this di dalam closure, dan delegate adalah objek tambahan penerima pencarian nama yang secara default menunjuk pada owner. Pada closure tingkat skrip, ketiganya menunjuk objek skrip yang sama.
// Tiga properti closure: owner, delegate, dan thisObject
def periksa = { 'dipanggil' }
println "owner == thisObject : ${periksa.owner.is(periksa.thisObject)}"
println "owner == delegate : ${periksa.owner.is(periksa.delegate)}"
class Layanan {
String nama
def penyebut = { nama }
}
def layanan = new Layanan(nama: 'piket')
println "Pemanggilan : ${layanan.penyebut()}"
println "owner == layanan : ${layanan.penyebut.owner.is(layanan)}"owner == thisObject : true
owner == delegate : true
Pemanggilan : piket
owner == layanan : trueBaris keempat memperlihatkan perilaku yang paling sering dipakai. Closure penyebut membaca properti nama tanpa awalan apa pun, dan nama itu terselesaikan pada Layanan karena owner closure tersebut adalah instans Layanan yang memilikinya. Inilah alasan closure yang disimpan sebagai properti kelas dapat memakai properti kelasnya seperti kode biasa.
resolveStrategy: lima strategi pencarian#
Ketika closure memakai sebuah nama, Groovy memeriksa owner dan delegate mengikuti strategi yang ditetapkan pada properti resolveStrategy. Mekanisme ini hanya berlaku pada nama yang ditulis tanpa penerima, sedangkan akses berawalan seperti objek.properti langsung menuju objeknya. Tabel berikut merangkum kelima strateginya.
| Strategi | Urutan pencarian |
|---|---|
Closure.OWNER_FIRST | owner lebih dahulu, lalu delegate |
Closure.DELEGATE_FIRST | delegate lebih dahulu, lalu owner |
Closure.OWNER_ONLY | hanya owner, delegate diabaikan |
Closure.DELEGATE_ONLY | hanya delegate, owner diabaikan |
Closure.TO_SELF | hanya kelas closure itu sendiri |
Strategi default adalah OWNER_FIRST, sehingga pengaturan delegate saja tidak mengubah nama yang sudah ada pada owner. Default ini menjaga perilaku kode biasa, karena closure yang ditulis di dalam kelas tetap lebih dahulu melihat anggota kelasnya, dan delegate baru dikonsultasikan bila nama itu tidak ada. Kode berikut membandingkan kedua strategi utama pada situasi yang sama, yaitu kedua kelas memiliki properti judul.
// OWNER_FIRST dan DELEGATE_FIRST menentukan urutan pencarian
class Laporan {
String judul
def ringkas = { "Ringkasan: $judul" }
}
class Pengganti {
String judul
}
def laporan = new Laporan(judul: 'Piket Mingguan')
def pengganti = new Pengganti(judul: 'Jadwal Ujian')
println laporan.ringkas()
laporan.ringkas.delegate = pengganti
println "OWNER_FIRST : ${laporan.ringkas()}"
laporan.ringkas.resolveStrategy = Closure.DELEGATE_FIRST
println "DELEGATE_FIRST : ${laporan.ringkas()}"Ringkasan: Piket Mingguan
OWNER_FIRST : Ringkasan: Piket Mingguan
DELEGATE_FIRST : Ringkasan: Jadwal UjianDengan OWNER_FIRST, properti judul milik owner menang sehingga delegasi tidak terasa. Pencarian berpindah ke Pengganti hanya setelah strateginya menjadi DELEGATE_FIRST. Diagram berikut mengilustrasikan urutan pencarian kedua strategi tersebut.
Metode dari delegate#
Delegasi tidak berhenti pada properti, karena pemanggilan metode mengikuti strategi yang sama. Bila sebuah metode tidak ada pada owner sesuai urutan strategi, Groovy memeriksanya pada delegate. Pada strategi default, metode yang tidak dikenal skrip akan jatuh ke delegate secara otomatis.
// Metode yang tidak ada di owner dicari pada delegate
class PengaturPiket {
List<String> petugas = []
void bertugas(String nama) {
petugas << nama
}
}
def atur = {
bertugas 'Ali'
bertugas 'Budi'
bertugas 'Citra'
}
atur.delegate = new PengaturPiket()
atur()
println "Petugas: ${atur.delegate.petugas}"Petugas: [Ali, Budi, Citra]Closure atur memanggil bertugas yang tidak ada pada skrip. Karena strateginya OWNER_FIRST dan owner tidak memiliki metode tersebut, Groovy meneruskannya ke delegate, yaitu instans PengaturPiket. Daftar petugas pada delegate pun terisi tanpa satu pun penyebutan delegate di dalam closure.
Delegasi sebagai dasar DSL#
Pola pada bagian sebelumnya dibungkus menjadi satu fungsi untuk menghasilkan DSL yang mudah dibaca. Fungsi pembungkusnya menyiapkan objek target, memasangnya sebagai delegate, menetapkan DELEGATE_FIRST, lalu memanggil closure-nya. Ketiga langkah itu dilakukan sebelum closure dipanggil, karena perubahan setelah pemanggilan tidak berpengaruh apa pun. Di dalam blok DSL, penugasan hari = 'Selasa' dan pemanggilan bertugas 'Ali' terselesaikan pada objek target tanpa awalan.
// Mini-DSL: delegasi menjadi dasar sintaks builder
class JadwalPiket {
String hari = 'Senin'
List<String> petugas = []
void bertugas(String nama) {
petugas << nama
}
String toString() { "$hari: ${petugas.join(', ')}" }
}
def susun(Closure blok) {
def jadwal = new JadwalPiket()
blok.delegate = jadwal
blok.resolveStrategy = Closure.DELEGATE_FIRST
blok()
jadwal
}
def jadwal = susun {
hari = 'Selasa'
bertugas 'Ali'
bertugas 'Budi'
}
println jadwalSelasa: Ali, BudiStrategi DELEGATE_FIRST dipilih agar properti dan metode target mendahului milik skrip, sehingga blok DSL berperilaku seolah-olah berjalan di dalam objek target. Pemilihan ini menentukan hasil akhirnya, karena penugasan seperti hari = 'Selasa' pada strategi default akan menyimpan nilai ke skrip, sedangkan objek target tetap memakai nilai lamanya. DELEGATE_FIRST dengan demikian menjadi pilihan bila isi blok memang ditujukan kepada target, sedangkan OWNER_FIRST dipertahankan bila closure lebih banyak memakai anggota lingkungannya sendiri. Mekanisme yang sama menggerakkan MarkupBuilder dan builder lain pada pustaka Groovy, dan setiap panggilan tanpa penerima diarahkan ke builder.
Kesalahan umum#
Kesalahan pertama adalah memanggil metode yang tidak ada pada owner maupun delegate. Karena tidak ada tempat yang memiliki nama tersebut, Groovy melempar MissingMethodException saat closure dipanggil, dan pesannya mencantumkan nama yang dicari. Perbaikannya adalah memastikan metode itu ada pada salah satu pihak, atau memasang delegate yang tepat sebelum closure dipanggil.
def panggil = { hitungTotal() }
try {
panggil()
} catch (MissingMethodException e) {
println "Gagal: ${e.getClass().simpleName}"
}Gagal: MissingMethodExceptionKesalahan kedua adalah mengira delegate selalu menang. Pada strategi default OWNER_FIRST, nama yang sudah dimiliki owner tidak pernah sampai ke delegate, sehingga delegasi tampak tidak berfungsi tanpa pesan error apa pun. Nama yang belum dimiliki siapa pun tetap jatuh ke delegate, sehingga masalah jenis ini hanya menyerang nama yang kebetulan sama. Perbaikannya adalah menetapkan resolveStrategy = Closure.DELEGATE_FIRST bersamaan dengan penunjukan delegate, sebagaimana dilakukan pada berkas DSL di atas.
Ringkasan#
owneradalah objek yang melingkupi closure,delegateadalah objek target pencarian nama yang default-nya sama denganowner, danthisObjectmerujukthisdi dalam closure.resolveStrategymenentukan urutannya, denganOWNER_FIRSTsebagai default danDELEGATE_FIRSTsebagai pilihan utama untuk DSL.- Metode mengikuti strategi yang sama dengan properti, sehingga closure dapat memanggil metode delegate tanpa awalan.
- Kombinasi penunjukan delegate dan
DELEGATE_FIRSTdi dalam fungsi pembungkus menghasilkan blok DSL yang membaca seperti kalimat.
Referensi#
- Closures — Apache Groovy — bagian owner, delegate, dan this beserta tabel strategi delegasi yang menjadi rujukan perilaku topik ini.