Skip to content
Blog Programming

Bagaimana Saya Menjaga Kode Saya Tetap Terbaca bagi Orang Berikutnya

By Romi Fadli, S.Kom. 24 min read Software Design
On this page
A panel of code lines, some starting with highlighted names, connected by three curves to a person, a clock, and a chat window whose older history is faded above a dashed line.

Sebagian besar kode yang saya tulis akan dibaca orang yang tidak hadir ketika kode itu ditulis. Kadang ia rekan kerja yang melanjutkan service buatan saya. Lebih sering ia saya sendiri, enam bulan kemudian, tanpa ingatan kenapa sebuah fungsi berbentuk seperti itu. Dan makin sering pula ia AI agent yang membuka repository tanpa ingatan apa pun, membaca beberapa baris di sekitar hasil pencarian, lalu diharapkan mengubahnya dengan benar.

Saya bekerja di beberapa bahasa — Rust, Python, Node.js, PHP — dan aturan yang saya pakai supaya kode tetap terbaca bagi pembaca berikutnya itu sama di semuanya. Artikel ini menguraikan aturan-aturan itu beserta alasan saya memegangnya, dengan satu service kecil sebagai studi kasus. Service itu kebetulan ditulis dalam Rust. Aturannya tidak bicara soal Rust.

Pendahuluan

Kode jauh lebih sering dibaca daripada ditulis. Robert C. Martin menaksir perbandingan waktu membaca dan menulis kode jauh di atas sepuluh banding satu, dan sebabnya sederhana begitu disadari: hampir setiap perubahan dimulai dengan membaca. Sebelum menambah satu baris, saya membaca fungsi tempat baris itu masuk, fungsi-fungsi yang memanggilnya, dan tipe-tipe yang disentuhnya. Kalau membacanya lambat, menulisnya pun lambat, secepat apa pun saya mengetik. Biaya sebuah codebase tidak terletak pada versi pertamanya. Biaya itu ada di setiap perubahan sesudahnya, dan setiap perubahan itu dimulai dari seseorang yang berusaha memahami kode yang sudah ada.

Itulah seluruh argumen clean code dalam satu paragraf, dan argumen itu pula yang diajukan Martin dalam Clean Code: A Handbook of Agile Software Craftsmanship (2008). Buku itu berisi deretan panjang aturan praktis — cara memberi nama, seberapa besar fungsi sepatutnya, cara menangani error, kapan menulis komentar — dan tiap aturan melayani tujuan yang sama: membuat pembacaan berikutnya lebih murah. Tak satu pun aturan dasar di bawah ini saya ciptakan sendiri. Yang saya lakukan adalah menerapkannya bertahun-tahun di berbagai bahasa dan project, memperhatikan mana yang paling berpengaruh, lalu menambahkan satu argumen yang belum mungkin dibuat buku itu ketika ditulis.

A. Siapa orang berikutnya itu

Ketika saya menyebut "orang berikutnya", yang saya maksud tiga pembaca berbeda, dan kebutuhan ketiganya sedikit berlainan.

Pembaca pertama adalah rekan kerja. Ia paham bahasa dan framework-nya, tetapi tidak paham niat saya. Ia bisa bertanya, tapi hanya bila saya sempat, dan tiap pertanyaan yang terpaksa ia ajukan menandakan kodenya gagal menjawab sendiri.

Pembaca kedua adalah saya di kemudian hari — dan inilah pembaca yang paling sering diremehkan. Saya kenal gaya saya sendiri, tetapi detailnya sudah lupa: edge case mana yang memaksa percabangan aneh itu, kenapa satu nilai divalidasi di sini dan tidak di sana. Kode yang terasa jelas saat saya menulisnya berubah jadi teka-teki dalam hitungan minggu, karena yang membuatnya jelas adalah konteks di kepala saya, dan konteks itu tidak bertahan.

Pembaca ketiga adalah AI coding agent — language model yang bekerja di dalam repository dengan mencari file, membaca potongan, menjalankan perintah, dan menyunting kode secara berulang. Agent kini bagian dari cara kerja hampir semua programmer yang saya kenal, dan cara mereka membaca kode berbeda dari manusia dalam satu hal penting: mereka tak punya ingatan di luar apa yang sedang ada di depannya.

Batas itu punya nama. Context window sebuah model adalah jumlah teks yang bisa ia pegang sekaligus — percakapan, file yang sudah dibuka, keluaran perintah-perintahnya. Ketika sesi yang panjang memenuhi window itu, banyak tools menjalankan compaction: bagian percakapan yang lebih lama diganti ringkasan pendek supaya pekerjaan bisa berlanjut. Ringkasan itu menyimpan intinya dan membuang detailnya, dan detail yang terbuang sering kali justru definisi yang dibaca agent sejam sebelumnya. Sesi baru dimulai dengan window kosong. Berganti ke agent lain pun mulai dari nol. Di semua keadaan itu, agent berjumpa lagi dengan kode sebagai orang asing.

Agent juga jarang membaca satu file utuh. Ia mencari sebuah nama, membuka dua puluh atau empat puluh baris di sekitar tiap hasil, lalu bernalar dari potongan itu. Makna yang tidak tampak di baris-baris itu — di nama, di tipe, di komentar yang menempel pada keputusannya — saat itu memang tidak ada baginya.

B. Studi kasus: storage service bernama OSW

Contoh-contoh di artikel ini diambil dari OSW, service kecil yang saya rawat dan berdiri di depan object storage. Object storage adalah cara menyimpan file sebagai objek utuh di dalam wadah bernama yang disebut bucket, dicapai lewat HTTP dan bukan lewat filesystem. Antarmuka yang paling dikenal adalah Amazon S3, sedangkan MinIO adalah server self-hosted yang berbicara dengan protokol yang sama. Aplikasi berbicara ke OSW, tidak langsung ke storage-nya. OSW membagikan presigned URL — tautan sementara yang mengizinkan browser meng-upload atau men-download satu file tertentu tanpa pernah memegang kredensial storage — dan mengelola siklus hidup sederhana: upload baru mendarat di area sementara, dipindahkan ke tempat permanennya begitu aplikasi mengonfirmasi, dan digeser ke area terhapus ketimbang langsung dimusnahkan.

OSW cocok jadi studi kasus karena ia ada dalam dua versi. Versi lamanya service yang berjalan baik, kira-kira dua ribu baris, dengan seluruh logika di satu folder service yang datar. Tugasnya beres, tapi mengubahnya susah. Saya membangunnya ulang mengikuti aturan di artikel ini sambil menjaga setiap response tetap identik bagi aplikasi yang sudah memakainya. Syarat kedua itu penting di sini: artinya setiap beda antara kedua versi adalah beda keterbacaan, bukan beda perilaku. Keduanya Rust, tetapi persoalan di versi lama bukan persoalan Rust. Semuanya pernah saya jumpai di Python, JavaScript, dan PHP, dan perbaikannya bisa dipindahkan langsung — bagian Hasil menunjukkan caranya.

Versi lama itu tidak buruk dalam arti yang lazim. Tak ada yang sembrono di dalamnya. Ia ditulis oleh orang yang memahami seluruh service pada saat menulis, dan persis itulah yang membuatnya sulit dibaca oleh siapa pun yang tidak memahaminya.

C. Lima hambatan yang menghalangi

Jadi tujuannya mudah dirumuskan: menulis kode yang bisa dipahami pembaca berikutnya — rekan kerja, saya di masa depan, atau agent tanpa ingatan — dari kodenya sendiri, cukup cepat sehingga mengubahnya tetap murah, apa pun bahasa yang dipakai project itu.

Gampang dirumuskan, canggung dijalankan. Kebanyakan kode yang sulit dibaca bukan lahir dari kecerobohan, melainkan dari lima kebiasaan yang masing-masing terasa masuk akal saat itu, dan masing-masing punya jalan keluar yang tampak jelas tapi sebenarnya tidak berhasil.

Hambatan pertama: nama yang menyembunyikan maksudnya. Semua fungsi di OSW versi lama bernama execute, dan nama tipenya berakhiran Entity entah ia memang entity atau bukan. Obat yang langsung terpikir adalah dokumentasi: README atau halaman wiki yang menjelaskan semuanya. Saya menolaknya, karena dokumentasi tinggal di tempat lain, dan pembaca yang sedang melihat satu pemanggilan tidak melihatnya. Nama adalah satu-satunya penjelasan yang ikut terbawa bersama kode ke setiap tempat ia dipakai.

Hambatan kedua: fungsi yang mengerjakan lebih dari satu hal. Satu fungsi membuat presigned URL dari delapan argumen terpisah. Fungsi lain menghapus file dengan salah satu dari dua cara yang sangat berbeda, tergantung sebuah boolean. Pembelaan yang biasa: memecahnya hanya menambah jumlah fungsi yang harus diingat. Padahal fungsi yang perilakunya ditentukan flag adalah dua fungsi yang berbagi nama, dan pemanggilan dengan delapan argumen berurutan tak bisa dibaca tanpa membuka signature-nya.

Hambatan ketiga: error yang berpindah sebagai teks. Ketika file tidak ada, versi lama mengembalikan error yang pesannya diawali NOT_FOUND:, dan lapisan HTTP memeriksa awalan itu untuk memutuskan 404. Godaannya adalah menganggap semuanya baik karena berjalan. Ia berjalan hanya selama dua potong kode di dua file berbeda sepakat soal satu string, dan tak ada yang menjaga kesepakatan itu — bukan compiler, bukan test, bukan pembaca yang cuma melihat salah satu sisinya.

Hambatan keempat: alasan di balik kode tidak ada di dalam kode. Banyak baris di storage service ada karena alasan yang tak terlihat dari barisnya: validasi yang sengaja diletakkan di awal, URL yang dibangun dengan cara tak lazim karena signature-nya bergantung pada itu. Jalan yang biasa diambil orang adalah tanpa komentar sama sekali ("kodenya harus bicara sendiri") atau komentar yang mengulang apa yang dikerjakan kode. Yang pertama kehilangan alasannya; yang kedua basi begitu kodenya pertama kali berubah. Di sampingnya ada ketiadaan test, yang membuat pembaca tak bisa membedakan perilaku mana yang disengaja dan mana yang kebetulan.

Hambatan kelima: pembaca berikutnya mungkin tak ingat apa-apa. Semua aturan di atas ditulis untuk manusia, yang setidaknya makin akrab dengan sebuah codebase seiring waktu. AI agent tidak. Pemahamannya dibangun ulang dari nol setiap kali context-nya di-compact, dikosongkan, atau diserahkan ke tools lain. Jalan pintasnya — menulis penjelasan panjang di percakapan atau di file instruksi untuk agent — gagal dengan sebab yang sama seperti dokumentasi: letaknya terpisah dari baris yang sedang dibaca, dan teks semacam itulah yang paling dulu dibuang compaction.

Urutannya penting. Nama didahulukan karena semua yang lain dibangun darinya: fungsi dinilai dari namanya, tipe error adalah sebuah nama, dan test sebagian besar berupa nama yang diikuti assertion. Hambatan kelima diletakkan terakhir karena ia bukan teknik tersendiri. Ia alasan baru kenapa keempat hambatan sebelumnya kini lebih penting daripada ketika buku itu ditulis.

Metode

A. Nama yang menyatakan maksudnya

Inilah jawaban atas hambatan pertama: saya memilih nama supaya pembaca yang hanya melihat tempat pemanggilannya sudah paham apa yang terjadi, tanpa membuka definisinya. Deskripsi clean code dari Ward Cunningham, yang dikutip Martin, jadi ujian saya: kita sedang membaca clean code bila setiap rutin ternyata kurang lebih persis seperti yang kita duga dari namanya.

Contoh paling jelas di OSW adalah siklus hidup file. Di versi lama, siklus itu hanya ada sebagai string literal: "temporary/" di satu fungsi, "deleted/" di tiga fungsi lain, masing-masing digabung atau dipotong dengan tangan. Pembaca harus menemukan semua kemunculannya untuk paham bahwa string-string itu bersama-sama membentuk state machine. Sekarang konsep itu punya nama sendiri:

rust
// Posisi sebuah objek dalam siklus hidupnya saat ini.
pub enum Lifecycle {
    Permanent,
    Temporary,
    Deleted,
}

impl Lifecycle {
    // Bangun path lengkap dengan menempelkan prefix siklus ini di depan `tail`.
    pub fn apply(self, tail: &str) -> String { /* … */ }

    // Lepas prefix siklus ini, atau `None` bila path-nya tak membawanya.
    pub fn strip(self, path: &str) -> Option<&str> { /* … */ }
}

Baris seperti Lifecycle::Deleted.apply(&path) terbaca sebagai kalimat. Ia menunjukkan bahwa file punya status, bahwa "deleted" salah satunya, dan bahwa status itu tersimpan di path — tak satu pun pernah disampaikan literal "deleted/".

Aturan yang sama saya pakai untuk nama tipe. Sufiks Entity saya buang karena ia menjelaskan di mana tipe itu diletakkan, bukan apa tipe itu: FileMetadataEntity menjadi FileMetadata, dan enam fungsi bernama execute menjadi use case yang dinamai menurut kerjanya — CommitTemporaryFile, RestoreFile, GeneratePresignedUrl. Saya selalu memilih nama panjang ketimbang nama pendek. Nama panjang menambah beberapa karakter di tiap pemakaian; nama yang kabur menambah satu perjalanan ke definisi di tiap pemakaian, dan yang menanggung perjalanan itu pembacanya, bukan penulisnya.

B. Fungsi yang mengerjakan satu hal

Fungsi sepatutnya mengerjakan satu hal, pada satu tingkat detail, dengan argumen yang cukup sedikit untuk dibaca tanpa dicari. Fungsi pembuat presigned URL dulu menerima delapan argumen terpisah. Pemanggilannya berupa deretan nilai yang maknanya sepenuhnya ditentukan posisi. Saya menggantinya dengan satu input bernama:

rust
pub struct GeneratePresignedUrlCommand {
    pub content_type: String,
    pub permission: String,
    pub count: i64,
    pub path: String,
    pub bucket: String,
    pub is_temporary: bool,
}

Kini tiap nilai berlabel di mana pun command itu dibangun, dan dependensi yang tak pernah berubah antar-pemanggilan — client storage, clock, generator ID — pindah ke constructor use case, tidak lagi dioper di setiap pemanggilan. Bentuk yang sama ada di setiap bahasa yang saya pakai: dataclass di Python, object biasa bertipe di TypeScript, class kecil readonly di PHP.

Flag argument adalah separuh lain dari aturan ini. Keberatan Martin: parameter boolean mengumumkan bahwa fungsinya mengerjakan dua hal, satu ketika flag bernilai true dan satu lagi ketika false. OSW masih punya satu, dan saya lebih suka mengatakannya terus terang daripada menampilkan gambaran yang lebih rapi dari kodenya: DeleteFile bercabang berdasarkan is_soft_delete. Yang berubah, flag itu kini berada di dalam command bernama, bukan sebagai true telanjang di deretan argumen, dan tiap cabang mengembalikan hasil dengan nama berbeda — DeleteOutcome::soft_deleted atau DeleteOutcome::hard_deleted — sehingga pembaca selalu tahu jalur mana yang berjalan. Memecahnya jadi dua use case adalah langkah wajar berikutnya. Bedanya, flag yang tersisa kini terlihat dan bernama, dan justru itu yang membuat langkah tadi gampang diambil.

C. Error yang membawa maknanya dalam tipe

Hambatan ketiga punya penyelesaian paling rapuh di versi lama. Bentuknya kurang lebih begini:

rust
// versi lama, disederhanakan ke bentuknya
return Err(anyhow!("NOT_FOUND:file does not exist"));

// … di file lain, di lapisan HTTP
if msg.starts_with("NOT_FOUND:") {
    let text = msg.trim_start_matches("NOT_FOUND:");
    return not_found(text);
}

Tak ada yang menghubungkan kedua belahan itu kecuali string yang kebetulan dieja sama oleh keduanya. Ubah satu, dan file yang tidak ada diam-diam berubah jadi server error. Saya mengganti teks itu dengan tipe yang menamai setiap cara sebuah use case bisa gagal:

rust
pub enum UseCaseError {
    // Input gagal validasi (satu atau lebih error per field).
    Validation { messages: Vec<String> },
    // Objek yang diminta tidak ada.
    NotFound { path: String, bucket: String },
    // Masalah deployment, mis. bucket tak ada di pool.
    Configuration(String),
    // Selebihnya dari lapisan storage atau jaringan.
    Storage { message: String, source: Option<anyhow::Error> },
}

Penerjemahan ke HTTP kini terjadi di tepat satu fungsi di tepi service, dan compiler memastikan tiap varian ditangani:

rust
pub fn translate(err: UseCaseError, operation: &str) -> HttpResponse {
    match err {
        UseCaseError::Validation { messages } => bad_request(messages),
        UseCaseError::NotFound { path, bucket } => not_found(&path, &bucket),
        UseCaseError::Configuration(_) | UseCaseError::Storage { .. } => {
            server_error(operation)
        }
    }
}

Aturan yang sama muncul di bab penanganan error buku Martin, sebagai anjuran memakai exception ketimbang kode kembali, dan sebagai nasihat untuk membekali error dengan konteks yang cukup untuk ditindaklanjuti. Di bahasa yang punya exception, padanannya adalah hierarki kecil class exception yang ditangkap di satu tempat. Yang penting bukan mekanismenya, melainkan bahwa makna kegagalan tinggal di dalam konstruksi yang bisa diperiksa bahasanya, dan bukan di teks yang hanya dibandingkan manusia.

D. Komentar untuk alasan, test untuk perilaku

Martin menulis bahwa pemakaian komentar yang tepat adalah untuk menutupi kegagalan kita mengungkapkan diri lewat kode. Saya setuju soal komentar yang menjelaskan apa yang dikerjakan kode: kalau kode membutuhkannya, berarti nama atau strukturnya keliru, dan komentar itu akan tertinggal zaman begitu seseorang pertama kali menyunting baris di bawahnya. Saya berbeda pendapat dengan pembacaan kaku atas aturan itu pada komentar yang mencatat kenapa. Tak ada nama yang bisa memikul alasan. Ini satu contoh dari OSW:

rust
pub fn new(pool: BucketPool, public_origin: String) -> Result<Self> {
    // Divalidasi di sini, bukan tiap kali presign, supaya
    // `s3_public_origin` yang rusak menggagalkan startup, bukan request pertama.
    Url::parse(&public_origin)
        .map_err(|e| anyhow!("invalid s3_public_origin '{}': {}", public_origin, e))?;
    // …
}

Tanpa komentar itu, pembaca yang sedang merapikan kode bisa saja memindahkan pemeriksaannya ke tempat nilai itu dipakai, dan service akan menyala dengan setelan rusak lalu gagal di request sungguhan pertamanya. Komentar itu membuat keputusannya terlihat tepat di titik tempat keputusan itu bisa dibatalkan.

Test menjawab separuh lainnya: perilaku mana yang disengaja. Saya menamai test sebagai kalimat tentang perilaku, sehingga daftar nama test terbaca seperti spesifikasi, dan saya menjalankan use case terhadap versi in-memory dari dependensinya supaya test tak butuh storage server sungguhan:

rust
#[tokio::test]
async fn soft_delete_copies_then_deletes_and_returns_legacy_message() {
    let repo = InMemoryFileRepo::new()
        .with_object("private", "uploads/a.jpg")
        .into_arc();
    let uc = DeleteFile::new(repo.clone(), Arc::new(TestConfig));
    // …
}

Bagian kedua itu bergantung pada satu pilihan arsitektur: use case berbicara ke storage lewat interface — port, dalam kosakata Clean Architecture — dan tidak memanggil client S3 secara langsung. Di versi lama, service memanggil fungsi storage secara langsung, sehingga test mana pun untuknya butuh storage server yang menyala. Dengan test terpasang, merapikan kode tak lagi berisiko, dan itulah yang membuat perapian bisa berlangsung terus-menerus, bukan sesekali lewat penulisan ulang yang menegangkan.

E. Kode yang bertahan di depan pembaca tanpa ingatan

Hambatan terakhir inilah tempat pengalaman saya melampaui buku itu. Saya bekerja dengan AI agent setiap hari, dan yang berulang kali saya lihat: mutu perubahan yang dibuat agent sangat ditentukan oleh seberapa banyak makna yang dibawa kode di sekitarnya dengan sendirinya.

Bayangkan yang dilihat agent ketika diminta memperbaiki 404 yang berubah jadi 500. Di OSW versi lama, pencarian akan menemukan msg.starts_with("NOT_FOUND:"). Baris itu baru separuh cerita; separuhnya lagi ada di file mana pun yang menghasilkan string tersebut. Kalau agent sudah membaca file itu lebih awal di sesi yang sama, pengetahuannya mungkin sudah hilang dimakan compaction. Di sesi baru, ia memang tak pernah memilikinya. Perbaikan yang tampak masuk akal adalah menyesuaikan teks di sisi yang terlihat — yang diam-diam mematahkan setiap tempat lain yang bergantung pada ejaan lama. Di OSW sekarang, pencarian yang sama menemukan UseCaseError::NotFound { path, bucket }. Maknanya ada di tipe, tipenya diperiksa compiler, dan perubahan apa pun yang dibuat agent di satu sisi tanpa dicocokkan sisi lain akan gagal di-build, bukan gagal di production.

Jadi saya menambahkan satu ujian pada aturan-aturan di atas: bisakah pembaca memahami baris ini seandainya hanya baris inilah yang ia baca hari ini? Nama seperti Lifecycle::Deleted lolos. Literal "deleted/" tidak. Command struct dengan field berlabel lolos; delapan argumen berurutan tidak. Komentar yang menyatakan alasan di samping keputusannya lolos; penjelasan yang ditulis sebelumnya di percakapan yang kini sudah diringkas habis tidak. Ternyata clean code persis sifat yang membuat kehilangan konteks jadi tak berbahaya: ketika setiap baris menjelaskan dirinya, kehilangan sisa konteks hanya menelan sedikit biaya.

Ada bonus praktisnya juga. Agent menemukan kode dengan mencari, dan nama yang khas membuat pencarian tepat sasaran. Mencari Lifecycle::Deleted mengembalikan setiap tempat status terhapus dipakai dan tak ada yang lain; mencari "deleted/" ikut mengembalikan pesan log, komentar, dan path yang tak berhubungan di samping yang memang penting.

Hasil dan Pembahasan

A. Yang berubah di OSW

Membangun ulang OSW dengan cara ini mengubah strukturnya secara besar-besaran tanpa menyentuh perilaku luarnya. Setiap response yang diandalkan aplikasi tetap sama, sampai ke susunan kata pesan error-nya, dan deskripsi API yang diterbitkan tetap identik. Seluruh perbedaannya ada di sisi pembaca:

AspekVersi lamaVersi sekarang
Use caseenam fungsi bernama execute dalam satu folder dataruse case bernama, dikelompokkan per resource: file, presigned URL, temporary
Helper responsedua belas fungsi yang nyaris kembarsatu presenter
Validasi bucketpemeriksaan yang sama ditulis lima kalisatu modul validasi
Errorteks berawalan NOT_FOUND:tipe error empat varian, diterjemahkan ke HTTP di satu tempat
Akses storagedipanggil langsung dari servicedi balik port, dengan versi in-memory untuk test
Testtak satu use case pun bisa diuji tanpa storage server98 test, tak satu pun butuh server yang menyala
Yang berubah di antara dua versi OSW, dengan perilaku luarnya dijaga tetap.

Ukuran paling berguna justru tidak ada di tabel. Saya menambahkan dua fitur sesudahnya — copy di sisi server antar-bucket, dan endpoint batch yang menandatangani hingga seratus tautan download dalam satu request — dan masing-masing mendarat di tempat-tempat yang sama dan bisa ditebak: tipe input, use case dengan test-nya sendiri, tipe hasil di domain, method pada adapter storage bila memang perlu, handler, dan satu baris wiring. Strukturnya sudah menunjukkan letak tiap bagian sebelum saya menulisnya.

B. Aturan yang sama di bahasa lain

Tak satu pun dari semua ini khas Rust. Aturannya soal pembaca, dan setiap bahasa punya wujud untuknya:

AturanRustPythonTypeScript (Node.js)PHP
Namai konsep, jangan ulang literalenumEnumstring union atau enumenum (8.1+)
Satukan banyak argumen jadi satu inputstruct@dataclassparameter object bertipereadonly class
Bawa makna kegagalan dalam tipeenum errorclass exceptionsubclass Errorclass exception
Bergantung pada port, bukan clienttraitProtocol atau ABCinterfaceinterface
Empat aturan yang sama dalam empat bahasa yang paling sering saya pakai.

Di service Python yang dibangun di atas FastAPI, misalnya, aturan error berwujud seperti ini — exception bernama, dan satu penerjemahan di tepi:

python
class NotFound(Exception):
    def __init__(self, path: str, bucket: str):
        super().__init__(f"file '{path}' does not exist in bucket '{bucket}'")
        self.path, self.bucket = path, bucket

# satu penerjemahan ke HTTP, di tepi service
@app.exception_handler(NotFound)
async def not_found(request: Request, err: NotFound):
    return JSONResponse(status_code=404, content={"errors": [str(err)]})

Yang berbeda adalah seberapa besar bantuan bahasanya dalam menegakkan aturan itu. Compiler Rust menolak match yang melupakan satu varian error; di Python, jaminan yang sama harus datang dari test. Aturannya tidak berubah mengikuti tooling. Yang berubah hanya jaring pengamannya.

C. Yang tak bisa diantisipasi buku itu

Clean Code terbit tahun 2008, jauh sebelum AI coding agent ada. Pembacanya programmer manusia, dan argumennya berkisar pada perhatian dan ingatan manusia. Argumen-argumen itu masih berlaku. Namun kini hampir semua programmer yang saya kenal bekerja berdampingan dengan agent, artinya sebagian besar pembacaan di sebuah codebase dilakukan oleh pembaca dengan kelemahan yang ganjil: ia lupa.

Pandangan saya, keadaan ini membuat buku itu makin relevan, bukan berkurang. Ketiadaan ingatan agent adalah versi ekstrem dari persoalan yang sudah dipecahkan Martin. Rekan kerja manusia lupa sedikit demi sedikit; agent lupa sepenuhnya, di setiap compaction, setiap sesi baru, setiap perpindahan ke tools lain. Aturan yang dirancang supaya kode bisa dipahami tanpa konteks penulisnya ternyata persis aturan yang membuat kode bisa dipahami tanpa konteks sama sekali. Nama yang baik adalah satu-satunya penjelasan yang masih ada ketika semua yang lain sudah diringkas habis.

Kebalikannya pun berlaku, dan bagian inilah yang paling saya rasakan dalam kerja sehari-hari. Codebase yang sulit dibaca bukan cuma lebih lambat bagi agent; ia lebih berbahaya. Manusia yang tak paham satu baris biasanya berhenti dan bertanya. Agent cenderung menghasilkan perubahan yang tampak masuk akal lalu melanjutkan. Makin sedikit kode menjelaskan dirinya, makin banyak perubahan masuk akal itu yang keliru dengan cara yang baru disadari belakangan.

D. Di mana saya berhenti

Clean code punya ongkos, dan saya berusaha menjaganya tetap sepadan. Mengganti nama demi mengganti nama menghasilkan diff besar yang mempersulit penelusuran riwayat, jadi saya mengganti nama ketika memang sedang mengubah kodenya atau ketika sebuah nama benar-benar menyesatkan. Saya tidak memecah fungsi hanya karena melewati batas jumlah baris yang sembarang; saya memecahnya ketika ia mengerjakan dua hal. Dan, seperti ditunjukkan flag di DeleteFile, saya lebih memilih ketidaksempurnaan yang terlihat dan bernama daripada yang tersembunyi. Tujuannya bukan codebase yang mematuhi setiap aturan, melainkan codebase yang pembaca berikutnya bisa mengerjakannya tanpa perlu bertanya apa pun kepada saya.

Simpulan

Saya menulis kode untuk pembaca yang datang berikutnya: rekan kerja, saya sendiri berbulan-bulan kemudian, atau AI agent yang berjumpa dengan kode sebagai orang asing setiap kali context-nya direset. Aturan yang saya pakai adalah aturan Robert C. Martin, dan semuanya bekerja dengan cara yang sama di Rust, Python, TypeScript, dan PHP: nama yang menyatakan maknanya, fungsi yang mengerjakan satu hal dengan sedikit argumen, error yang maknanya tinggal di dalam tipe, komentar yang mencatat alasan, dan test yang mencatat perilaku. OSW memperlihatkan dampaknya dalam praktik — perilaku yang sama, dicapai lewat kode yang bisa dibaca satu potongan demi satu potongan.

Satu gagasan yang ingin saya tambahkan pada buku itu adalah alasan kenapa semua ini kini lebih penting. Setiap aturan yang menolong manusia yang tak punya konteks penulis juga menolong pembaca yang tak punya konteks apa pun, dan pembaca semacam itu kini hadir di hampir setiap codebase, setiap hari. Kode yang terbaca adalah satu-satunya konteks yang bertahan.

Istilah

Clean code
Kode yang ditulis supaya pembaca berikutnya bisa memahami dan mengubahnya dengan murah. Istilahnya berasal dari buku Robert C. Martin berjudul sama.
AI coding agent
Language model yang bekerja di dalam repository kode dengan mencari, membaca potongan, menjalankan perintah, dan menyunting file secara berulang.
Context window
Jumlah teks yang bisa dipegang dan dinalar language model dalam satu waktu.
Compaction
Penggantian bagian lama sebuah sesi agent yang panjang dengan ringkasan pendek supaya sesi bisa berlanjut; detailnya, termasuk definisi yang dibaca sebelumnya, ikut hilang.
Object storage
Storage yang menyimpan file sebagai objek utuh di dalam bucket bernama, dicapai lewat HTTP. Amazon S3 menetapkan antarmuka yang umum; MinIO adalah server self-hosted yang kompatibel dengannya.
Presigned URL
Tautan sementara yang mengizinkan satu upload atau download tertentu tanpa memberikan kredensial storage kepada client.
Flag argument
Parameter boolean yang mengalihkan sebuah fungsi di antara dua perilaku berbeda.
Port
Interface tempat logika aplikasi menjangkau sistem luar, sehingga sistem itu bisa diganti — misalnya dengan versi in-memory saat test.

Technologies covered

  • Python
  • Rust

About the author

Photo of Romi Fadli

Romi Fadli, S.Kom.

Solution Architect · Backend Engineer · Embedded Systems

In my day job I work as a solution architect. I design the system, lead the team that builds it — quality assurance, backend developers, a UI developer, DevOps and DevSecOps — and write the most critical modules myself, such as a certificate authority, a long-term PDF signer, and the core API of a personnel system.

I work across the whole stack that a running system actually needs. On the hardware side that means PCB design, microcontrollers, and embedded firmware — including RISC-V boards running FreeRTOS and Embassy, speaking MQTT over mutually authenticated TLS on a microcontroller with 64 KB of SRAM.

Most of my day-to-day work is software: Rust backends and API modules built on Clean Architecture, an OAuth2/OpenID Connect authorization server written from scratch, PAdES document signing, and certificate authorities — root CAs, timestamping authorities, and OCSP responders. I also build the frontends that sit on top of them.

Running all of that means infrastructure: Docker Compose stacks, networking, Mikrotik and RADIUS, observability with OpenTelemetry, and a steady habit of writing command-line tools and utilities for Linux when the existing ones do not fit.

Share

Enjoyed this?

Get an email when a new article is published.

Referenced by

A stack of Markdown documents and an orange arrow leading to a browser page with a side menu, a heading, paragraphs, a dark code panel, and a table of contents with its active entry highlighted

Satu script yang mengubah dokumentasi Markdown satu project menjadi situs statis yang terbuka dari folder tanpa server maupun internet: menu, daftar isi yang mengikuti pembaca, tabel yang tak pernah terpotong, kode dengan tombol salin, dan tautan yang tetap berfungsi.

7 min read Read more →
A browser window connected directly to a storage bucket by a thick green band, while an orange box with a key above them hands the browser a dashed link.

Service Rust kecil di antara aplikasi dan storage kompatibel S3: ia membagikan presigned link supaya browser berbicara langsung dengan storage, memberi tiap file siklus hidup, menyalin antar-bucket di sisi server, dan menjauhkan kredensial storage dari setiap aplikasi.

12 min read Read more →

Related Articles

Four flat layers stacked on the left — routes, controllers, services, repositories — joined by short lines, with an orange line labelled later: refactor leading to the concentric circles of Clean Architecture on the right

Satu folder per jenis kode di satu tingkat, jalur pendek dari route ke repository, satu file per operasi — bentuk yang saya berikan pada service Rust yang harus cepat jadi atau muat di konteks AI agent lokal, sebelum tumbuh jadi Clean Architecture.

7 min read Read more →