Rancang DSL lebih dulu

Merancang tata bahasa dan transformasi DSL sebelum implementasi.

Daftar isi

Rancangan sebuah DSL (domain-specific language) dimulai dari kalimat pemakaiannya, bukan dari kelas implementasinya, karena DSL adalah kode yang memang dibaca seperti kalimat. Topik ini menerapkan prinsip tersebut pada proyek pemandu, yaitu CLI (command line interface) rekap statistik nilai ujian yang tersusun atas pustaka inti dan antarmuka baris perintah picocli. Konfigurasi alat itu dirancang dari sudut pandang pembaca skripnya. Pembahasannya bergerak dari kalimat pemakaian ideal, tata bahasa beserta kosakata yang sah, pemetaan kalimat ke metode implementasi, sampai transformasi kalimat menjadi map aturan untuk inti program. Sebuah implementasi kecil menguji rancangan tersebut dengan membuktikan bahwa kalimat yang disepakati benar-benar membentuk struktur data yang diinginkan, sedangkan kalimat di luar tata bahasa ditolak sendiri oleh Groovy tanpa kode pemeriksa tambahan.

Kalimat target ditulis lebih dahulu#

Langkah pertama perancangan DSL bukan menulis kelas, melainkan menulis contoh pemakaian yang ideal. Contoh itu berupa skrip konfigurasi utuh yang disusun dari sudut pandang pembacanya, yaitu orang yang kelak menyunting aturan rekap tanpa membaca kode implementasinya. Kalimat-kalimat dalam skrip tersebut menetapkan tiga hal sekaligus, yaitu kosakata yang tersedia, bentuk tiap kalimatnya, dan struktur data yang harus dihasilkan implementasi. Skrip kecil ini dengan demikian berperan sebagai spesifikasi, sedangkan kelas yang menyusul hanya pekerjaan membuat spesifikasi itu benar.

groovy Kalimat target: skrip konfigurasi yang diinginkan
// tidak-dijalankan
// Kalimat target: bentuk pemakaian ideal, ditulis sebelum implementasi ada
rekap {
    hari 'selasa'
    mata 'matematika'
    batas 70
}

Setiap pilihan di dalamnya dapat dipertanggungjawabkan. Kata rekap menyebut hasil akhirnya tanpa awalan teknis, sedangkan hari, mata, dan batas dipinjam dari kosakata catatan nilai ujian, bukan dari nama variabel ataupun metode implementasi. Nilai argumennya pun nyata: hari pengambilan catatan, mata pelajaran, dan ambang kelulusan 70. Blok ini sengaja tidak dijalankan pada tahap rancangan karena rekap memang belum ada, sehingga kedudukannya sebagai dokumen rancangan yang menjadi rujukan seluruh keputusan sesudahnya.

Menulis kalimat lebih dahulu juga membuat perubahan menjadi murah. Memindahkan batas menjadi kalimat terpisah, menyingkat nama kata, atau membuang salah satu aturan hanya menggeser satu baris teks, sementara keputusan yang sama setelah implementasi berdiri berarti membongkar tanda tangan metode beserta ujiannya. Rancangan dianggap selesai ketika kalimat target terbaca alami dan tidak ada satu pun kata yang meragukan artinya.

Tata bahasa kalimat rekap#

Tata bahasa DSL ini sengaja dibuat kecil. Satu berkas konfigurasi memuat tepat satu blok rekap, dan di dalamnya setiap kalimat berbentuk kata argumen, yaitu satu kata kosakata diikuti satu argumen. Setiap kalimat ditulis satu per baris tanpa tanda baca tambahan di ujungnya. Ketiga katanya berperan sebagai kata perintah meskipun asalnya kata benda, sedangkan tanda pemisah antarkalimat adalah pergantian baris itu sendiri, sehingga skrip konfigurasinya terbaca seperti daftar pernyataan pendek. Tabel berikut merangkum seluruh kosakata yang sah beserta jenis argumennya.

KataArgumen yang sahPerannya dalam rekap
harisatu string dalam tanda kutip, misalnya 'selasa'memilih hari catatan nilai yang direkap
matasatu string dalam tanda kutip, misalnya 'matematika'menetapkan mata pelajaran yang direkap
batassatu angka tanpa tanda kutip, misalnya 70menetapkan ambang kelulusan

Ketiga kata wajib hadir tepat satu kali agar map aturan yang terbentuk selalu lengkap, sedangkan kata apa pun di luar tabel bukan bagian tata bahasa ini dan pasti ditolak. Penolakan itu tidak perlu ditulis sendiri pada versi pertama, karena Groovy melempar MissingMethodException ketika sebuah kata tidak ditemukan pada objek penerimanya, sebagaimana dibuktikan pada bagian implementasi minimal. Struktur bloknya berdiri di atas mekanisme delegasi closure yang sudah dibahas sepanjang jalur, yaitu penugasan delegate disertai strategi DELEGATE_FIRST. Seluruh panggilan di dalam blok dengan demikian dieksekusi pada objek aturannya tanpa satu pun penyebutan penerimanya.

Pembatasan jumlah kata juga merupakan bagian dari rancangan. Tiga kata terasa sedikit, tetapi kosakata yang pendek mudah dihafal, mudah diperiksa, dan memperkecil peluang dua kata beririsan makna. Kebutuhan baru tidak otomatis menambah kosakata, karena banyak hal masih terjawab dengan nilai argumen yang berbeda. Kata yang benar-benar baru hanya ditambahkan setelah kebutuhannya tidak dapat dinyatakan dengan kosakata yang ada.

Pemetaan kalimat ke metode implementasi#

Pemetaan dari kalimat ke kode dibuat satu-satu agar tabel tata bahasa sekaligus menjadi daftar pekerjaan implementasinya. Setiap kata kosakata memiliki tepat satu metode dengan nama yang sama, sedangkan tanda tangan metodenya menetapkan jenis argumen yang sah. Disiplin ini membuat dua tabel pada topik ini saling mengunci, yaitu kalimat yang sah di sisi tata bahasa dan metode yang wajib ada di sisi implementasi.

Kalimat DSLMetode yang mewadahinyaHasil pada objek aturan
rekap { … }def rekap(Closure blok)objek AturanRekap dibuat, closure dijalankan, lalu objeknya dikembalikan
hari 'selasa'void hari(String nama)properti hari berisi "selasa"
mata 'matematika'void mata(String nama)properti mata berisi "matematika"
batas 70void batas(int angka)properti batas berisi 70

Tanda tangan metode juga menegakkan jenis argumen tanpa alat pemeriksa tambahan. Pemanggilan batas '70' yang mengirim string tidak cocok dengan void batas(int) dan berakhir sebagai MissingMethodException, sehingga kesalahan jenis argumen terlihat segera saat skrip dijalankan. Baris pertama tabel menunjukkan bahwa struktur blok ditangani satu fungsi pembungkus yang pekerjaannya selalu sama, yaitu membuat objek aturan, memasang delegasi, menjalankan closure-nya, lalu mengembalikan objek tersebut. Sisa implementasinya berupa kelas biasa dengan properti polos, sehingga tidak ada kejutan teknis saat kelasnya ditulis.

Transformasi menjadi map aturan#

Tugas DSL berakhir pada saat aturannya terbentuk, dan inti program tidak pernah bertemu closure sama sekali. Hasil transformasinya berupa satu map polos yang menjadi kontrak antara lapisan DSL dan lapisan perhitungan, misalnya [hari:selasa, mata:matematika, batas:70] untuk skrip target di awal topik ini. Kontrak berbentuk map dipilih karena strukturnya mudah dicetak, mudah dibandingkan, dan dapat diuji tanpa menyiapkan closure apa pun. Inti program juga sengaja tidak menerima objek AturanRekap secara langsung, karena kelas itu milik lapisan DSL dan perubahannya tidak boleh merambat ke lapisan perhitungan.

groovy Contoh: map aturan yang dipakai inti program
// map aturan: struktur data yang disepakati dengan inti program
def aturan = [hari: 'selasa', mata: 'matematika', batas: 70]
println aturan
println "Nilai 65 dinyatakan lulus : ${65 >= aturan.batas}"
println "Nilai 70 dinyatakan lulus : ${70 >= aturan.batas}"
text Keluaran
[hari:selasa, mata:matematika, batas:70]
Nilai 65 dinyatakan lulus : false
Nilai 70 dinyatakan lulus : true

Keluaran tersebut memperlihatkan cara inti program memakai kontraknya. Map aturan dicetak utuh agar mudah diperiksa, lalu nilai batas 70 menentukan kelulusan dengan menolak nilai 65 dan menerima nilai 70. Perhitungan statistik yang sesungguhnya, misalnya banyak peserta yang lulus atau rata-rata seluruh nilai, tinggal membaca map yang sama tanpa perlu mengetahui bahwa aturan itu pernah ditulis sebagai kalimat DSL.

Implementasi minimal rancangan#

Implementasi minimalnya berupa satu kelas dan satu fungsi pembungkus yang ditulis persis mengikuti tabel pemetaan. Kelas AturanRekap menyimpan tiga properti, menyediakan tiga metode satu baris sesuai kosakatanya, lalu menambahkan metode sebagaiMap sebagai pintu keluar menuju kontrak map. Fungsi rekap mengerjakan baris pertama tabel, yaitu membuat objek aturan, memasang delegate, menetapkan DELEGATE_FIRST, menjalankan closure-nya, lalu mengembalikan objek tersebut. Tidak ada mekanisme lain yang dibutuhkan pada tahap ini, sehingga ukuran implementasinya sebanding dengan ukuran tata bahasanya. Berkas aturan-rekap.groovy pada topik ini dapat dijalankan langsung dengan perintah groovy aturan-rekap.groovy, dan keluarannya sama pada setiap eksekusi karena tidak ada masukan dari luar.

groovy aturan-rekap.groovy
// aturan-rekap.groovy: implementasi minimal dari kalimat DSL yang dirancang
class AturanRekap {
    String hari
    String mata
    int batas

    void hari(String nama) { this.hari = nama }
    void mata(String nama) { this.mata = nama }
    void batas(int angka)  { this.batas = angka }

    Map sebagaiMap() { [hari: hari, mata: mata, batas: batas] }
}

def rekap(Closure blok) {
    def aturan = new AturanRekap()
    blok.delegate = aturan
    blok.resolveStrategy = Closure.DELEGATE_FIRST
    blok()
    aturan
}

def aturan = rekap {
    hari 'selasa'
    mata 'matematika'
    batas 70
}

println aturan.sebagaiMap()

// kalimat dengan kata di luar tata bahasa ditolak Groovy sendiri
try {
    rekap {
        hari 'selasa'
        mapel 'matematika'
    }
} catch (MissingMethodException e) {
    println "Ditolak: ${e.getClass().simpleName} pada kata ${e.method}"
}
text Keluaran
[hari:selasa, mata:matematika, batas:70]
Ditolak: MissingMethodException pada kata mapel

Baris pertama keluaran persis menyamai map aturan pada bagian sebelumnya, dan itulah bukti bahwa rancangan serta implementasinya saling cocok. Bagian kedua skrip memperlihatkan batas tata bahasanya. Kata mapel tidak terdaftar pada kosakata dan tidak dimiliki kelas AturanRekap, sehingga Groovy melempar MissingMethodException, sedangkan properti method milik exception itu menyebut kata yang bermasalah. Penolakan semacam ini datang dari bahasanya sendiri, sehingga versi pertama implementasi tidak membutuhkan kode validasi apa pun.

Kesalahan umum#

Kesalahan pertama adalah memadatkan dua kalimat ke dalam satu baris di dalam blok. Bentuk mata 'matematika' batas 70 terlihat efisien, tetapi parser Groovy menguraikannya sebagai satu rantai (command chain), yaitu mata('matematika').batas(70), sehingga kalimat kedua dijalankan atas hasil kalimat pertama. Karena metode mata tidak mengembalikan apa pun, panggilan batas jatuh ke objek null dan program berhenti dengan NullPointerException yang pesannya tidak menunjuk baris mana yang salah. Perbaikannya berupa disiplin penulisan satu kalimat per baris, atau rancangan ulang tata bahasa secara sadar bila bentuk rantai memang diinginkan.

groovy Contoh: dua kalimat pada satu baris diurai sebagai satu rantai
class AturanRekap {
    void mata(String nama) { println "mata terbaca: $nama" }
}
def rekap(Closure blok) {
    def aturan = new AturanRekap()
    blok.delegate = aturan
    blok.resolveStrategy = Closure.DELEGATE_FIRST
    blok()
    aturan
}
try {
    rekap { mata 'matematika' batas 70 }
} catch (NullPointerException e) {
    println "Gagal: ${e.getClass().simpleName}"
}
text Keluaran
mata terbaca: matematika
Gagal: NullPointerException

Baris mata terbaca: matematika pada keluaran menunjukkan kalimat pertama sempat dijalankan, sedangkan kegagalannya baru muncul ketika rantai mencoba melanjutkan ke batas.

Kesalahan kedua adalah membiarkan kosakata mengikuti artefak implementasi. Kata seperti setHari atau tambahMata terasa wajar bagi penulis kelasnya, tetapi keduanya membocorkan struktur kode ke dalam kalimat yang seharusnya milik pembaca konfigurasi, dan skripnya berubah menjadi daftar nama metode yang harus dihafal. Perbaikannya ada pada uji baca sederhana, yaitu membaca ulang skrip konfigurasi tanpa melihat kode implementasinya, lalu mengganti setiap kata yang artinya tidak langsung jelas. Kosakata juga dijaga tetap minimal dengan makna satu kata untuk satu hal, sehingga tabel tata bahasa tidak tumbuh melebihi kebutuhan yang sudah terbukti.

Ringkasan#

  • Rancangan DSL dimulai dari kalimat pemakaian ideal yang ditulis sebelum implementasi, karena kosakata dan bentuk kalimatnya ditentukan pembaca skrip konfigurasi, bukan penulis pustakanya.
  • Tata bahasa rekap berupa satu blok berisi kalimat kata argumen dengan kosakata hari, mata, dan batas, sedangkan kata di luar itu ditolak Groovy dengan MissingMethodException.
  • Setiap kalimat dipetakan satu-satu ke metode dengan nama sama, dan hasil akhirnya berupa satu map aturan polos yang menjadi kontrak dengan inti program.
  • Implementasi minimal membuktikan rancangannya, yaitu kalimat yang sah membentuk map yang dijanjikan, sedangkan dua kalimat yang dipadatkan dalam satu baris berakhir sebagai NullPointerException.

Referensi#

  • Groovy DSLs — Apache Groovy — dokumentasi resmi teknik pembuatan DSL Groovy yang mencakup command chain dan delegasi closure sebagai dasar bentuk blok rekap.
  • Closures — Apache Groovy — dokumentasi delegate, resolveStrategy, dan strategi delegasi yang menjalankan kalimat di dalam blok konfigurasi.