Command chain dan builder

Rantai metode tanpa titik dan builder yang memungkinkan DSL yang mudah dibaca.

Daftar isi

Command chain adalah fitur sintaks Groovy yang membolehkan beberapa pemanggilan metode ditulis berurutan dalam satu baris tanpa titik dan tanpa tanda kurung, sehingga kodenya terbaca seperti kalimat. Topik ini membahas aturan penulisan rantai tersebut, cara parser menguraikannya menjadi panggilan bersarang, serta pembuatan builder sendiri dalam dua bentuk, yaitu kelas dengan metode rantai dan builder dengan closure yang didelegasikan. Contoh yang dipakai berupa pemilih jadwal pelajaran dan pembuat laporan piket, sehingga setiap mekanisme dapat langsung dijalankan dan diperiksa keluarannya.

Sintaks command chain#

Sebuah command chain tersusun atas metode dan argumen yang berselang-seling tanpa titik dan tanpa kurung. Parser menguraikannya berpasangan dari kiri ke kanan, sehingga bentuk a b c d setara dengan a(b).c(d). Operand yang mengakhiri tiap pasangan boleh berupa string literal dalam tanda kutip, angka, atau variabel yang sudah dideklarasikan. Identifier tanpa kutip bukan string, melainkan nama variabel yang dicari nilainya, sehingga penulisan hari senin berbeda makna dengan hari 'senin'. Rantai lazim ditulis pada pernyataan tingkat atas, dan dokumentasi resmi Groovy mencatat bahwa rantai juga sah pada ruas kanan penugasan. Metode tanpa argumen di tengah atau di ujung rantai tetap ditulis dengan tanda kurung.

Skrip berikut meniru pola resmi pada dokumentasi DSL Groovy. Metode tolong menerima closure aksi dan mengembalikan map yang entrinya juga closure, sedangkan tampilkan dan akarKuadrat adalah variabel closure yang menjadi operand rantai.

groovy rantai-dasar.groovy
// rantai-dasar.groovy: metode dan argumen berselang-seling tanpa titik dan kurung
def tolong(aksi) {
    [tunjukkan: { pengubah ->
        [nilai: { angka -> aksi(pengubah(angka)) }]
    }]
}
def tampilkan = { nilai -> println "angka: $nilai" }
def akarKuadrat = { Math.sqrt(it) }

// setara dengan: tolong(tampilkan).tunjukkan(akarKuadrat).nilai(100)
tolong tampilkan tunjukkan akarKuadrat nilai 100
text Keluaran
angka: 10.0

Alurnya sebagai berikut. Panggilan tolong tampilkan mengeksekusi tolong(tampilkan) dan menghasilkan map yang memuat entri tunjukkan. Panggilan tunjukkan akarKuadrat menjalankan closure entri tersebut dan menghasilkan map berikutnya yang memuat entri nilai. Panggilan terakhir nilai 100 mengeksekusi aksi dengan hasil akarKuadrat(100), sehingga keluarannya adalah akar kuadrat dari 100. Pemanggilan metode pada map yang nilai entrinya closure memang menjalankan closure itu, dan pola inilah yang membuat rantai dapat dibentuk hanya dengan map dan closure.

Ragam operand pengakhirnya terlihat pada rantai berikut, yang ditulis pada ruas kanan penugasan dan memakai variabel hariIni, literal string 'selasa', serta angka sebagai argumen.

groovy Contoh: operand literal, variabel, dan angka
// operand pengakhiri berupa variabel, literal string, dan angka
def jadwal = [hari: { h -> [sesi: { n -> "piket $h sesi $n" }] }]
def hariIni = 'senin'

def pertama = jadwal.hari hariIni sesi 1
println pertama
def kedua = jadwal.hari 'selasa' sesi 2
println kedua
text Keluaran
piket senin sesi 1
piket selasa sesi 2

Rantai pertama memakai variabel hariIni sebagai operand, rantai kedua memakai literal string, dan keduanya mengirim angka sebagai argumen sesi. Urutan penguraiannya tetap berpasangan dari kiri, sehingga jadwal.hari hariIni selesai lebih dahulu dan sesi 1 dipanggil pada hasilnya. Ketiga bentuk operand itu dapat dicampur dalam satu rantai sepanjang variabel yang dirujuk sudah dideklarasikan.

Penguraian menjadi panggilan bersarang#

Karena rantai diurai secara mekanis, bentuk rantai dan bentuk titik-kurung pada kode yang sama menghasilkan keluaran yang identik. Rantai berjumlah genap berakhir pada pemanggilan metode, sedangkan rantai berjumlah ganjil berakhir pada akses properti sebagaimana dicatat dokumentasi resmi, misalnya take 3 cookies yang diurai menjadi take(3).cookies. Kelas Pengingat berikut dipakai untuk membuktikannya. Metode ingatkan menambah pesan, metode pada melengkapi waktu, dan susun() merangkai seluruh pesan menjadi satu baris teks.

groovy Contoh: dua bentuk pada kode yang sama
// dua bentuk penulisan pada kode yang sama
class Pengingat {
    List<String> teks = []
    def ingatkan(pesan) { teks << pesan; this }
    def pada(waktu) { teks[-1] = "${teks[-1]} pada $waktu"; this }
    String susun() { teks.join(', ') }
}

def a = new Pengingat().ingatkan('rapat tim').pada('jam 9 pagi').susun()
def b = new Pengingat().ingatkan 'rapat tim' pada 'jam 9 pagi' susun()
println "Bentuk titik-kurung : $a"
println "Bentuk rantai       : $b"
println "hasil identik       : ${a == b}"
text Keluaran
Bentuk titik-kurung : rapat tim pada jam 9 pagi
Bentuk rantai       : rapat tim pada jam 9 pagi
hasil identik       : true

Keluaran menunjukkan kedua bentuk berakhir pada isi yang sama. Rantai pada baris b diurai menjadi ingatkan('rapat tim').pada('jam 9 pagi').susun(), sehingga perbandingan a == b menghasilkan true karena keduanya String berisi teks yang sama. Perbedaan hanya ada pada penulisan, bukan pada hasil eksekusinya. Satu hal yang perlu dijaga adalah posisi penulisan rantai.

Pemilih jadwal dengan metode rantai#

Builder pertama dibangun sebagai kelas yang metodenya menerima satu argumen, menyimpan nilai tersebut, lalu mengembalikan this. Nilai kembalian this membuat panggilan berikutnya tetap ditujukan ke objek yang sama, sehingga rantai dapat berlanjut sepanjang yang diinginkan. Metode yang mengganti nilai cukup menimpa atributnya, sedangkan metode yang mengumpulkan nilai seperti mata menambahkan isinya ke list dan karenanya boleh dipanggil lebih dari sekali. Rantai ditutup dengan susun(), yaitu metode tanpa argumen yang tetap memakai kurung dan menghasilkan String akhir.

groovy pemilih-jadwal.groovy
// pemilih-jadwal.groovy: builder kelas dengan metode rantai
class PemilihJadwal {
    def hariTerpilih
    def jamTerpilih
    def mataPelajaran = []
    def hari(nama) { hariTerpilih = nama; this }
    def jam(jam) { jamTerpilih = jam; this }
    def mata(nama) { mataPelajaran << nama; this }
    String susun() { "jadwal $hariTerpilih jam $jamTerpilih: ${mataPelajaran.join(', ')}" }
}

def jadwalA = new PemilihJadwal().hari 'senin' jam 8 mata 'matematika' susun()
println jadwalA

def pilih = new PemilihJadwal()
def jadwalB = pilih.hari 'selasa' jam 10 mata 'fisika' mata 'kimia' susun()
println jadwalB
text Keluaran
jadwal senin jam 8: matematika
jadwal selasa jam 10: fisika, kimia

Kedua jadwal dibuat dari objek PemilihJadwal yang berbeda agar isinya tidak tercampur. Jadwal pertama memilih satu mata pelajaran, sedangkan jadwal kedua merantai mata 'fisika' mata 'kimia' sehingga kedua pelajaran terkumpul mengikuti urutan pemanggilannya. Bila sebuah metode rantai lupa mengembalikan this, panggilan berikutnya ditujukan ke nilai kembalian yang salah dan rantai berakhir dengan MissingMethodException.

Builder dengan closure dan delegasi#

Builder kedua menyembunyikan objek pembangun di dalam closure. Fungsi buatLaporan menerima closure konfigurasi, menugaskan properti delegate milik closure ke objek PembuatLaporan, mengatur resolveStrategy ke DELEGATE_FIRST, lalu menjalankan closure itu. Penugasan delegate beserta strategi resolusinya adalah mekanisme delegasi closure Groovy, yaitu pengalihan pencarian metode dan properti closure kepada objek lain. Berkat delegasi tersebut, panggilan judul dan catat di dalam closure dieksekusi pada objek PembuatLaporan, bukan pada lingkup skrip.

groovy pembuat-laporan-piket.groovy
// pembuat-laporan-piket.groovy: builder dengan closure dan delegasi
class PembuatLaporan {
    String teksJudul = ''
    List<String> catatan = []
    def judul(String teks) { teksJudul = teks; this }
    def catat(String isi) { catatan << "  $isi"; this }
    String susun() { ([teksJudul] + catatan).join('\n') }
}

def buatLaporan(Closure konfigurasi) {
    def pembuat = new PembuatLaporan()
    konfigurasi.delegate = pembuat
    konfigurasi.resolveStrategy = Closure.DELEGATE_FIRST
    konfigurasi()
    pembuat.susun()
}

def laporan = buatLaporan {
    judul 'Laporan Piket Kelas 8A'
    catat 'Senin: Ali menyapu lantai'
    catat 'Selasa: Budi merapikan meja'
    catat 'Rabu: Citra menyiram tanaman'
}

println laporan
text Keluaran
Laporan Piket Kelas 8A
  Senin: Ali menyapu lantai
  Selasa: Budi merapikan meja
  Rabu: Citra menyiram tanaman

Teks judul menjadi baris pertama karena disusun lebih dahulu, sedangkan setiap panggilan catat menambahkan baris berindentasi dua spasi di bawahnya. Bentuk ini cocok untuk isi DSL yang berupa blok konfigurasi panjang, karena objek pembangun dan seluruh urutan pemanggilannya berada di satu tempat yang sama. Memanggil buatLaporan lagi dengan closure serupa menghasilkan laporan baru yang strukturnya sama, karena setiap pemanggilan menerima objek PembuatLaporan yang segar.

Kesalahan umum#

Kesalahan pertama adalah merantai metode yang tidak dimiliki oleh hasil rantai sebelumnya. Rantai hanya berjalan bila setiap metode berikutnya tersedia pada penerima terkini, sehingga pemanggilan minggu pada objek yang tidak memilikinya melempar MissingMethodException.

groovy Contoh: rantai tak cocok
// rantai tak cocok: metode berikutnya tidak dimiliki penerima
class PemilihJadwal {
    def hari(nama) { this }
}

def p = new PemilihJadwal()
try {
    p.hari 'senin' minggu 3
} catch (MissingMethodException e) {
    println "Gagal: ${e.getClass().simpleName} pada metode ${e.method}"
}
text Keluaran
Gagal: MissingMethodException pada metode minggu

Properti method pada exception menyebut nama metode yang gagal dicari. Pada contoh, hari mengembalikan objek PemilihJadwal itu sendiri, sehingga pencarian minggu berhenti di kelas tersebut dan pesannya menunjuk minggu. Perbaikannya adalah menyamakan nama-nama metode pada rantai dengan metode yang benar-benar tersedia pada kelas atau map penerimanya.

Kesalahan kedua adalah menulis operand tanpa kutip yang tidak pernah dideklarasikan. Identifier semacam itu dicari sebagai variabel, dan ketiadaannya memicu MissingPropertyException saat rantai dievaluasi.

groovy Contoh: operand tidak terdefinisi
// operand tanpa kutip wajib variabel yang sudah dideklarasikan
def tolong(aksi) {
    [tunjukkan: { pengubah ->
        [nilai: { angka -> aksi(pengubah(angka)) }]
    }]
}
def tampilkan = { nilai -> println "angka: $nilai" }

try {
    tolong tampilkan tunjukkan akarKuadrat nilai 100
} catch (MissingPropertyException e) {
    println "Gagal: ${e.getClass().simpleName} pada nama ${e.property}"
}
text Keluaran
Gagal: MissingPropertyException pada nama akarKuadrat

Properti property melaporkan nama variabel yang tidak ditemukan. Pada rantai-dasar.groovy, rantai yang sama berjalan karena akarKuadrat sudah dideklarasikan sebagai closure di atasnya. Perbaikannya ada dua, yaitu mendeklarasikan variabel operand terlebih dahulu, atau menuliskannya sebagai string literal bila maksudnya memang teks.

Ringkasan#

  • Command chain menuliskan metode dan argumen berselang-seling tanpa titik dan kurung, dan bentuk a b c d diurai menjadi a(b).c(d).
  • Operand pengakhir berupa string literal, angka, atau variabel yang sudah dideklarasikan, sedangkan metode tanpa argumen di ujung rantai tetap berkurung dan rantai berjumlah ganjil berakhir pada akses properti.
  • Rantai sah pada pernyataan tingkat atas dan ruas kanan penugasan, tetapi tidak di dalam daftar argumen panggilan lain.
  • Builder kelas mengembalikan this dari setiap metode rantai, sedangkan builder closure menugaskan delegate dan resolveStrategy agar panggilan di dalam closure dieksekusi pada objek pembangun.
  • Rantai yang tidak cocok melempar MissingMethodException, dan operand tanpa kutip yang tidak terdefinisi melempar MissingPropertyException.

Referensi#

  • Groovy DSLs — Apache Groovy — bagian Command chains pada dokumentasi resmi DSL Groovy, berisi aturan penguraian rantai dan contoh pembuatan DSL dengan map serta closure.
  • Semantics — Apache Groovy — aturan optional parentheses pada pemanggilan metode yang menjadi dasar bentuk pemanggilan tanpa kurung.
  • Closures — Apache Groovy — dokumentasi delegate closure dan delegation strategy yang menggerakkan builder berbasis closure.