Topik ini merancang layanan antrean pekerjaan percetakan kampus dari sisi data terlebih dahulu, sebelum satu pun fungsi aplikasi ditulis. Rancangannya mencakup schema kontrak data dengan spec.alpha, batasan domain sebagai spec gabungan, pilihan state untuk setiap bagian sistem, dan pohon proyek minimal. Setelah rancangan ini teruji, setiap fungsi aplikasi berikutnya ditulis terhadap bentuk data yang pasti, bukan terhadap dugaan.
Prinsip desain data-first#
Pendekatan data-first menempatkan bentuk data sebagai keputusan pertama dalam sebuah proyek. Pendekatan ini cocok dengan sifat Clojure yang menulis program sebagai rangkaian transformasi atas struktur data, karena setiap keputusan lain, yaitu fungsi, state, dan pengujian, mengikuti bentuk data yang dipilih. Pada layanan antrean percetakan, satu pekerjaan adalah satu map, sehingga pertanyaan pertamanya bukan fungsi apa yang ditulis, melainkan map seperti apa yang sah.
Bentuk data yang sah dinyatakan dengan schema, yaitu deskripsi struktur dan batasan yang wajib dipenuhi sebuah data. Deskripsi tersebut ditulis dengan spec, yaitu fitur Clojure untuk mendefinisikan dan memeriksa bentuk data. Ada dua keuntungan yang diperoleh sekaligus. Schema berfungsi sebagai dokumentasi yang tidak pernah basi karena dapat diperiksa, dan pemeriksaan berjalan otomatis di gerbang penerimaan data sehingga error terlihat pada saat data masuk, bukan di tengah algoritma.
Desain data-first juga menurunkan biaya keputusan berikutnya. Fungsi pengolah pekerjaan hanya perlu menangani bentuk yang sudah dinyatakan, data yang tidak sah ditolak sebelum sempat merusak state, dan pengujian dapat menghasilkan data contoh dari spec yang sama. Karena itu, waktu yang dihabiskan pada bagian ini terbayar pada seluruh bagian sesudahnya.
Schema data pekerjaan#
Kontrak data ditetapkan per kunci menggunakan s/def atas keyword berkualifikasi (qualified keyword) dengan awalan :antrean/, lalu kunci-kunci tersebut digabungkan menjadi satu spec map dengan s/keys. Penamaan berkualifikasi memisahkan kunci milik antrean dari kunci milik bagian lain, sehingga tidak terjadi tabrakan ketika data dari sumber berbeda digabungkan. Satu pekerjaan membawa identitas, nama dokumen, prioritas, jumlah halaman, perkiraan durasi cetak dalam detik, dan status. Definisikan spec tersebut, uji dengan contoh data yang sah dan yang rusak, lalu amati pesan penjelasannya.
Buat berkas schema.clj dengan isi berikut, lalu jalankan dengan clojure -M schema.clj.
;; schema data pekerjaan layanan antrean percetakan kampus
;; dijalankan dengan: clojure -M schema.clj
(require '[clojure.spec.alpha :as s])
(s/def :antrean/id string?)
(s/def :antrean/dokumen (s/and string? not-empty))
(s/def :antrean/prioritas (s/int-in 1 6))
(s/def :antrean/halaman pos-int?)
(s/def :antrean/durasi-detik pos-int?)
(s/def :antrean/status #{:menunggu :jalan :selesai :gagal})
(s/def :antrean/pekerjaan
(s/keys :req-un [:antrean/id
:antrean/dokumen
:antrean/prioritas
:antrean/halaman
:antrean/durasi-detik
:antrean/status]))
(def pekerjaan-valid
{:id "PJC-101" :dokumen "soal-midterm-a" :prioritas 1
:halaman 12 :durasi-detik 180 :status :menunggu})
(def pekerjaan-rusak
{:id "PJC-104" :dokumen "" :prioritas 9
:halaman -3 :durasi-detik 0 :status :tertunda})
(println "Valid?" (s/valid? :antrean/pekerjaan pekerjaan-valid))
(println "Rusak?" (s/valid? :antrean/pekerjaan pekerjaan-rusak))
(s/explain :antrean/pekerjaan pekerjaan-rusak)Valid? true
Rusak? false
"" - failed: not-empty in: [:dokumen] at: [:dokumen] spec: :antrean/dokumen
9 - failed: (int-in-range? 1 6 %) in: [:prioritas] at: [:prioritas] spec: :antrean/prioritas
-3 - failed: pos-int? in: [:halaman] at: [:halaman] spec: :antrean/halaman
0 - failed: pos-int? in: [:durasi-detik] at: [:durasi-detik] spec: :antrean/durasi-detik
:tertunda - failed: #{:gagal :menunggu :jalan :selesai} in: [:status] at: [:status] spec: :antrean/statusFungsi s/valid? mengembalikan satu nilai boolean, sedangkan s/explain mencetak satu baris penjelasan untuk setiap kunci yang bermasalah. Setiap baris memuat nilai yang ditolak, predikat yang dilanggar, posisi kunci pada data, dan nama spec-nya. Contoh yang rusak sengaja memuat lima pelanggaran sekaligus agar kekuatan s/explain terlihat, yaitu seluruh kunci bermasalah dilaporkan dalam satu panggilan, bukan satu per satu. Baris terakhir menunjukkan cara membaca enum, himpunan nilai sah yang ditetapkan sebelumnya, yaitu :tertunda ditolak karena bukan anggotanya.
Batasan domain sebagai spec gabungan#
Tipe yang sah belum menjamin data yang masuk akal. Prioritas 9 sah sebagai int?, tetapi operator percetakan hanya mengenal lima tingkat. Durasi nol detik sah secara tipe, tetapi pekerjaan tanpa durasi tidak bermakna. Batasan domain (constraint) ditambahkan dengan menyusun predikat menjadi satu spec gabungan melalui s/and, s/int-in, dan pos-int?. Bentuk s/int-in menyatakan rentang setengah terbuka, sehingga (s/int-in 1 6) menerima 1 sampai 5 dan menolak 6.
Spec gabungan untuk kedua batasan tersebut diuji dengan fragmen berikut.
(require '[clojure.spec.alpha :as s])
;; prioritas lima tingkat: rentang setengah terbuka [1, 6)
(s/def :antrean/prioritas (s/int-in 1 6))
;; durasi cetak wajib positif, dalam detik
(s/def :antrean/durasi-detik pos-int?)
(println "Prioritas 1:" (s/valid? :antrean/prioritas 1))
(println "Prioritas 5:" (s/valid? :antrean/prioritas 5))
(println "Prioritas 6:" (s/valid? :antrean/prioritas 6))
(println "Durasi 30 detik:" (s/valid? :antrean/durasi-detik 30))
(println "Durasi 0 detik:" (s/valid? :antrean/durasi-detik 0))
(s/explain :antrean/prioritas 9)
(s/explain :antrean/durasi-detik 0)Prioritas 1: true
Prioritas 5: true
Prioritas 6: false
Durasi 30 detik: true
Durasi 0 detik: false
9 - failed: (int-in-range? 1 6 %) spec: :antrean/prioritas
0 - failed: pos-int? spec: :antrean/durasi-detikNilai 6 ditolak karena batas atas rentangnya eksklusif, dan pesan penjelasannya memuat predikat int-in-range? beserta rentang asalnya. Nilai 0 untuk durasi ditolak oleh pos-int? dengan pesan yang sama pendeknya karena spec-nya berdiri sendiri. Pesan semacam ini juga bermanfaat ketika batasan berubah, karena titik pemeriksaannya tetap satu, yaitu spec. Dengan cara ini, pengetahuan operator percetakan dipindahkan dari kebiasaan lisan menjadi kode yang dapat dijalankan ulang kapan saja.
Pilihan state per bagian sistem#
Kontrak data menjawab bentuk data, sedangkan state menjawab bagian mana yang berubah. Prinsip pemilihannya adalah state terkecil yang cukup. Nilai murni dipilih bila tidak ada identitas yang berubah, atom bila satu identitas berubah secara independen dan sinkron, channel bila ada penyerahan tugas antar thread, dan ref bila beberapa identitas wajib berubah bersamaan dalam satu transaksi. Pada lingkup minimal layanan antrean, transaksi antar identitas tidak muncul, sehingga ref belum diperlukan.
| Bagian sistem | Pilihan state | Alasan |
|---|---|---|
| Antrean yang sudah dijadwalkan | nilai murni berupa vector | sekali terurut, worker menarik dari urutan yang sama tanpa saling mengganggu |
| Status worker dan pekerjaan aktif | atom | diganti sinkron oleh worker yang menjalankannya dan dibaca oleh pemantau |
| Penyerahan tugas ke worker | channel | antrean pesan antar thread, worker menarik satu pekerjaan pada satu waktu |
| Total halaman tercetak per dokumen | atom berisi map | pembaruan independen per kunci dengan update |
Tabel tersebut membatasi keputusan sejak awal. Antrean terjadwal tetap murni sehingga penjadwalan mudah diuji, atom menampung perubahan yang menempel pada satu identitas, dan channel baru dibutuhkan pada saat ada dua thread yang benar-benar bekerja. Pemilihan yang keliru biasanya bukan persoalan kebenaran, melainkan persoalan koordinasi yang menjadi kaku, misalnya dua identitas yang seharusnya berurutan ditaruh dalam dua atom terpisah.
Pohon proyek minimal#
Rancangan data dan state diterjemahkan menjadi struktur direktori yang kecil. Berkas schema dipisahkan dari alur utama agar dapat dimuat untuk pengujian tanpa menjalankan sistem, sedangkan core.clj menjadi titik masuk yang menggabungkan penjadwalan dan alur kerjanya. Hanya deps.edn dan empat berkas Clojure yang dibutuhkan pada tahap ini.
antrean-percetakan/
├── deps.edn
├── src/
│ └── antrean/
│ ├── schema.clj ; spec kontrak data pekerjaan
│ ├── worker.clj ; pemrosesan satu pekerjaan dan statusnya
│ └── core.clj ; titik masuk, penjadwalan, dan alur utama
└── test/
└── antrean/
└── schema_test.clj ; pengujian kontrak data{:paths ["src"]
:deps {org.clojure/clojure {:mvn/version "1.12.6"}}}Berkas deps.edn menetapkan dua hal, yaitu :paths ["src"] sebagai akar sumber dan daftar dependensi yang berisi org.clojure/clojure saja. Ketiadaan dependensi lain adalah tujuan desain, bukan kelalaian, karena lingkup minimal tidak menuntut server maupun penyimpanan permanen. Struktur test/ mengikuti struktur src/ secara sejajar, sehingga setiap namespace produksi memiliki pasangan ujinya yang mudah ditemukan.
Isi berkas schema.clj yang sudah diuji pada bagian schema dipindahkan ke src/antrean/schema.clj. Perbedaannya satu, yaitu baris (ns antrean.schema) ditambahkan di bagian atas, sedangkan panggilan require berpindah ke dalam bentuk (:require [clojure.spec.alpha :as s]). Isi spec-nya sama persis, sehingga schema yang sudah teruji tetap menjadi acuan ketika digunakan oleh worker.clj dan core.clj.
Kesalahan umum#
Menulis fungsi lebih dahulu dan menemukan bentuk data di tengah jalan. Penemuan ini biasanya berupa kunci yang ditambahkan satu per satu ke map, sehingga fungsi yang sudah ditulis harus diperiksa ulang setiap kali bentuk datanya bergeser. Perbaikannya dengan menetapkan schema dan contoh datanya terlebih dahulu, sebelum fungsi pertama ditulis.
Menetapkan spec yang terlalu longgar atau terlalu ketat. Spec yang longgar seperti s/any? menampung apa saja sehingga pemeriksaan kehilangan daya, sedangkan spec yang terlalu ketat menolak data yang sah karena mengunci format yang belum pasti. Perbaikannya dengan memulai dari tipe dan batasan yang sudah diketahui domain, lalu longgarkan hanya bila ada data sah yang tertolak dengan bukti.
Menumpuk seluruh state dalam satu atom besar. Atom tunggal yang memuat antrean, status worker, dan hitungan membuat setiap pembaruan kecil menyentuh seluruh sistem, dan koordinasinya menjadi kaku. Perbaikannya dengan memisahkan state menurut tabel pilihan state di atas.
Ringkasan#
- Kontrak data satu pekerjaan dinyatakan dengan
s/defper kunci dans/keyssebagai spec map, dengan status berupa enum:menunggu,:jalan,:selesai, dan:gagal. - Batasan domain ditambahkan sebagai spec gabungan, yaitu prioritas satu sampai lima melalui
s/int-indan nilai positif melaluipos-int?. - Pilihan state mengikuti prinsip state terkecil yang cukup, yaitu nilai murni untuk antrean terjadwal, atom untuk status worker dan hitungan, channel untuk penyerahan tugas, dan
refyang belum diperlukan. - Pohon proyek minimal memisahkan schema, worker, dan core agar setiap bagian dapat diuji sendiri-sendiri.
Referensi#
- Guide to spec — Clojure
— Panduan resmi
s/def,s/keys,s/valid?, dans/explainyang digunakan pada schema. - Rationale — Clojure — Alasan desain Clojure yang menjadikan data sebagai pusat program.
- Data Structures — Clojure — Sifat struktur data persistent yang menjadi bahan kontrak data.