Delegasi dalam praktik

Hubungan owner, delegate, dan resolveStrategy yang menjadi dasar kerja DSL Groovy.

Daftar isi

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.

groovy owner-delegate.groovy
// 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)}"
text Keluaran
owner == thisObject : true
owner == delegate   : true
Pemanggilan         : piket
owner == layanan    : true

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

StrategiUrutan pencarian
Closure.OWNER_FIRSTowner lebih dahulu, lalu delegate
Closure.DELEGATE_FIRSTdelegate lebih dahulu, lalu owner
Closure.OWNER_ONLYhanya owner, delegate diabaikan
Closure.DELEGATE_ONLYhanya delegate, owner diabaikan
Closure.TO_SELFhanya 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.

groovy resolve-strategy.groovy
// 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()}"
text Keluaran
Ringkasan: Piket Mingguan
OWNER_FIRST    : Ringkasan: Piket Mingguan
DELEGATE_FIRST : Ringkasan: Jadwal Ujian

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

Urutan pencarian simbol pada strategi OWNER_FIRST dan DELEGATE_FIRSTOWNER_FIRST (strategi default)Simbol di closureownertidak adadelegateDELEGATE_FIRSTSimbol di closuredelegatetidak adaownerBila simbol tidak ditemukan pada kedua tempat, Groovy melemparMissingMethodException atau MissingPropertyException.
Urutan pencarian simbol pada strategi OWNER_FIRST dan DELEGATE_FIRST

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.

groovy delegate-method.groovy
// 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}"
text Keluaran
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.

groovy dsl-piket.groovy
// 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 jadwal
text Keluaran
Selasa: Ali, Budi

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

groovy Contoh: metode yang tidak ditemukan di mana pun
def panggil = { hitungTotal() }
try {
    panggil()
} catch (MissingMethodException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
text Keluaran
Gagal: MissingMethodException

Kesalahan 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#

  • owner adalah objek yang melingkupi closure, delegate adalah objek target pencarian nama yang default-nya sama dengan owner, dan thisObject merujuk this di dalam closure.
  • resolveStrategy menentukan urutannya, dengan OWNER_FIRST sebagai default dan DELEGATE_FIRST sebagai pilihan utama untuk DSL.
  • Metode mengikuti strategi yang sama dengan properti, sehingga closure dapat memanggil metode delegate tanpa awalan.
  • Kombinasi penunjukan delegate dan DELEGATE_FIRST di 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.