Sebagian Besar Bug Bersembunyi di Celah Antara “Seharusnya Begini” dan “Yang Sebenarnya Terjadi”
Anda menulis kode dulu, lalu menulis test, lalu menemukan kodenya salah. Itulah loop standar. Itulah sebabnya debugging menghabiskan setengah dari timeline proyek kebanyakan orang.
Box Structure membalikkan ini. Anda mendefinisikan perilaku sebelum menulisnya, memverifikasi definisi itu secara matematis, lalu menerjemahkannya ke kode satu lapisan demi satu lapisan. Hasilnya adalah module yang benar secara konstruksi, bukan benar karena test kebetulan lolos.
Ini terdengar mustahil sampai Anda melihat betapa kecilnya setiap lapisan.
Apa Itu Box Structure?
Box Structure menggambarkan sebuah module perangkat lunak pada tiga tingkat abstraksi, masing-masing adalah penyempurnaan ketat dari yang sebelumnya:
- Black box: Stimulus apa yang menghasilkan response apa? Tidak ada state. Tidak ada implementasi. Hanya pure function dari history ke output.
- State box: State apa yang diingat module, dan bagaimana setiap stimulus mentransformasi state tersebut dan menghasilkan response?
- Clear box: Kode sebenarnya.
Anda mendesain dari luar ke dalam. Black box adalah contract. State box adalah data model. Clear box adalah kode. Pada setiap langkah, Anda membuktikan bahwa lapisan bawah memenuhi lapisan atas sebelum melanjutkan.
Ini bukan dokumentasi setelah fakta. Black box dan state box adalah spesifikasi formal. Mereka adalah desain. Kode datang terakhir.
Black Box: History Menentukan Segalanya
Black box mendefinisikan module berdasarkan history stimulus dan responsenya. Response terhadap stimulus apa pun bergantung pada setiap stimulus yang datang sebelumnya.
Pertimbangkan sebuah token bucket rate limiter yang sederhana. Spesifikasi black box menyatakan:
| Stimulus | Condition on History | Response |
|---|---|---|
request(n) | tokens available >= n | grant(n) |
request(n) | tokens available < n | deny |
add_tokens(k) | any history | no response, update history |
Black box tidak menyatakan bagaimana token dilacak. Ia menyatakan: diberikan history stimulus ini, inilah response yang diperlukan.
Anda dapat memverifikasi ini sebelum ada kode apa pun. Tulis urutan stimulus, hitung response yang diharapkan secara manual, dan periksa apakah spesifikasi berperilaku dengan benar. Tidak ada compiler. Tidak ada test runner. Hanya logika.
State Box: Menambah Memori Tanpa Menambah Asumsi
State box menyempurnakan black box dengan memperkenalkan variabel state yang membuat history menjadi implisit. Alih-alih membawa history stimulus lengkap, module mengingat representasi terkompresi.
Untuk rate limiter, state box memperkenalkan tokens, jumlah token yang tersedia saat ini. Spesifikasinya menjadi:
| Stimulus | Precondition on State | Response | New State |
|---|---|---|---|
request(n) | tokens >= n | grant(n) | tokens - n |
request(n) | tokens < n | deny | tokens (unchanged) |
add_tokens(k) | any | no response | tokens + k |
Langkah kritis adalah membuktikan bahwa state box ini setara dengan black box. Variabel state tokens harus secara akurat merepresentasikan history yang relevan. Jika menerapkan aturan black box menghasilkan response yang sama dengan counter state box, maka penyempurnaan tersebut valid.
Bukti ini biasanya singkat. Anda memverifikasi spesifikasi satu halaman, bukan codebase beribu baris.
Clear Box: Kode yang Tidak Bisa Mengejutkan Anda
Clear box adalah implementasi. Ia ditulis dalam bahasa terstruktur tanpa goto dan tanpa hidden control flow. Setiap clear box dibangun dari sequence, alternation (if-then-else), dan iteration (while, for).
Berikut adalah implementasi clear box dalam Python:
class TokenBucket:
def __init__(self, capacity: int, refill_rate: float):
self.capacity = capacity
self.tokens = capacity
self.refill_rate = refill_rate
self.last_refill = time.monotonic()
def _refill(self) -> None:
now = time.monotonic()
elapsed = now - self.last_refill
added = int(elapsed * self.refill_rate)
if added > 0:
self.tokens = min(self.capacity, self.tokens + added)
self.last_refill = now
def request(self, n: int) -> str:
self._refill()
if self.tokens >= n:
self.tokens -= n
return f"grant({n})"
return "deny"
def add_tokens(self, k: int) -> None:
self.tokens = min(self.capacity, self.tokens + k)
Perhatikan bahwa _refill tidak ada di state box. Waktu tidak ada di black box atau state box. Model-model itu mengasumsikan token muncul secara ajaib ketika add_tokens dipanggil. Clear box harus menjembatani celah itu.
Di sinilah sebagian besar bug berada. State box menyatakan tokens bertambah sebesar k. Clear box juga harus menangani continuous refilling dan capacity cap sambil tetap sesuai dengan state box.
Bagaimana Cleanroom Membuktikan Kebenaran Tanpa Menjalankan Test
Dalam Cleanroom, Anda tidak menulis unit test untuk clear box. Anda melakukan verification.
Untuk setiap control structure, Anda menulis predicate yang harus berlaku sebelum dan sesudah eksekusi. Untuk sequence S1; S2, Anda membuktikan bahwa postcondition dari S1 mengimplikasikan precondition dari S2. Untuk if-then-else, Anda membuktikan kedua branch memenuhi postcondition keseluruhan. Untuk loop while, Anda menemukan invariant dan membuktikan invariant itu berlaku saat masuk, dipertahankan oleh setiap iterasi, dan mengimplikasikan postcondition yang diinginkan ketika loop berakhir.
Ini bukan proof assistant. Ini pensil dan kertas. Jika Anda pernah secara informal mengargumentasikan bahwa sebuah recursive function berakhir, Anda sudah melakukan 90 persen dari bukti Cleanroom.
Mengapa Sebagian Besar Tim Tidak Menggunakan Ini
Keberatan yang jelas adalah waktu. Menulis black box, state box, dan bukti tangan terdengar lebih lambat daripada sekadar menulis kode dan memperbaiki bug.
Harlan Mills, yang mengembangkan Cleanroom di IBM, mengukur sebaliknya. Tim Cleanroom mengirimkan kode dengan defect yang lebih sedikit satu orde of magnitude dibandingkan tim kontrol, dan total waktu pengembangan mereka lebih singkat karena mereka hampir tidak menghabiskan waktu di debugger.
Keberatan yang kurang jelas adalah budaya. Box Structure memaksa Anda untuk berpikir sebelum mengetik. Sebagian besar developer merasa ini tidak nyaman. Spesifikasi terasa seperti birokrasi. Bukan. Itu adalah desain. Dalam Cleanroom, desain ditulis dalam notasi yang cukup presisi untuk diverifikasi, bukan dalam diagram yang diabaikan oleh draft implementasi pertama.
Kapan Box Structure Sebanding dengan Overhead-nya
Anda tidak perlu menentukan setiap utility function dengan cara ini. Box Structure bersinar di mana bug lebih mahal daripada waktu untuk menulis spesifikasi: payment processing, authorization, distributed consensus, protocol, dan workflow engine. Mereka juga membantu tim yang berulang kali mengirimkan bug dalam kategori yang sama ke produksi dan tidak pernah menangkapnya dalam test.
Cara Ringan untuk Memulai
Anda tidak perlu mengadopsi seluruh proses Cleanroom. Pinjam ide Box Structure untuk satu module.
Pilih sebuah function yang telah menyebabkan masalah produksi. Tulis black box-nya: tabel input, condition, dan expected output. Jangan melihat kode yang ada. Tulis apa yang seharusnya dilakukan, bukan apa yang dilakukannya.
Kemudian tulis state box-nya. State apa yang dibutuhkannya? Bagaimana setiap input mentransformasi state tersebut? Bandingkan dengan implementasi Anda. Di mana mereka berbeda, Anda telah menemukan bug atau asumsi yang tidak didokumentasikan.
Berikut adalah template minimal dalam Python yang dapat Anda adaptasi:
"""
Black Box Specification: Rate Limiter
Stimuli: request(n), add_tokens(k)
History: sequence of all stimuli received
Rules:
1. After any sequence of stimuli, tokens available =
sum(add_tokens.k) - sum(grant.n)
2. request(n) grants iff tokens available >= n
3. tokens available never exceeds capacity
State Box Refinement:
State: tokens (integer, 0 <= tokens <= capacity)
Invariant: tokens accurately represents available tokens
"""
class TokenBucket:
"""Clear box implementation of the rate limiter state box."""
def __init__(self, capacity: int):
self.capacity = capacity
self.tokens = capacity
def request(self, n: int) -> str:
if n <= 0:
raise ValueError("request must be positive")
if self.tokens >= n:
self.tokens -= n
return f"grant({n})"
return "deny"
def add_tokens(self, k: int) -> None:
if k < 0:
raise ValueError("cannot add negative tokens")
self.tokens = min(self.capacity, self.tokens + k)
Komentar adalah spesifikasi. Kode adalah implementasi. Menjaga keduanya dalam file yang sama membuat penyempurnaan terlihat dan dapat direview.
Nilai Sebenarnya Adalah Separation of Concerns
Box Structure bukan sihir. Mereka tidak akan menangkap setiap bug. Apa yang mereka lakukan adalah memaksakan disiplin yang sebagian besar proses pengembangan lewati: mendefinisikan apa artinya benar sebelum Anda membangunnya.
Black box memisahkan contract module dari internalnya. State box memisahkan data model dari kode. Clear box memisahkan implementasi dari bukti. Setiap lapisan memiliki satu tugas, dan Anda memverifikasinya sebelum beralih ke lapisan berikutnya.
Itulah mengapa Anda harus peduli. Celah antara “apa yang seharusnya dilakukan” dan “apa yang dilakukan” adalah tempat bug Anda tinggal, dan Box Structure adalah cara sistematis untuk menutup celah itu sebelum Anda menulis satu test pun.
Jika Anda ingin mendalami, Cleanroom Software Engineering karya Mills dan laporan teknis IBM aslinya masih menjadi referensi paling jelas. Id-idenya sudah tua. Bug yang mereka cegah, tidak.