Kode yang sehat pada hari rilis pertama tidak selalu sehat dua tahun kemudian. Fitur baru ditambahkan, jalan pintas diambil, dan perlahan fungsi panjang serta cabang bersarang menumpuk. Refactoring adalah disiplin memperbaiki struktur internal kode tanpa mengubah perilaku luarnya — istilah yang dipopulerkan Martin Fowler dalam buku Refactoring. Output program sebelum dan sesudah refactoring harus identik; yang berubah hanyalah seberapa mudah kode dibaca dan dikembangkan berikutnya. Chapter ini mempraktikkan refactoring klasik dengan kode nyata beserta ritme kerja yang menjaganya tetap aman.
Langkah Pertama: Test Dulu #
Refactoring tanpa test adalah berjalan di gelap: Anda yakin tidak merusak apa pun, tetapi tidak bisa membuktikannya. Karena itu aturan pertamanya bersifat mutlak — sebelum menyentuh struktur, pastikan perilaku yang ada sudah dikunci oleh test. Section Testing dan Code Quality, khususnya Chapter Menulis Test dengan pytest, membahas cara menulis jaring pengaman itu. Ritme kerjanya sederhana:
- Jalankan test — harus hijau semua.
- Ubah satu hal pada struktur kode.
- Jalankan test lagi — harus tetap hijau.
- Ulangi.
Refactoring Klasik dengan Kode Nyata #
Ekstrak Fungsi dari Fungsi Panjang #
Function yang sekaligus memformat baris, menjumlahkan, dan merakit laporan sulit dibaca sekaligus sulit dites. Bentuk sebelum:
def buat_laporan(transaksi):
teks = ""
total = 0.0
for nama, jumlah in transaksi:
teks += f"{nama:<10}{jumlah:>10.0f}\n"
total += jumlah
teks += f"{'Total':<10}{total:>10.0f}\n"
return teksEkstrak dua niatnya menjadi function bernama, lalu susun ulang:
def format_baris(nama, jumlah):
"""Satu baris laporan dengan kolom rata kiri dan kanan."""
return f"{nama:<10}{jumlah:>10.0f}\n"
def hitung_total(transaksi):
"""Jumlahkan seluruh nilai transaksi."""
return sum(jumlah for _, jumlah in transaksi)
def buat_laporan(transaksi):
"""Susun laporan teks dari daftar transaksi."""
baris = [format_baris(nama, jumlah) for nama, jumlah in transaksi]
baris.append(format_baris("Total", hitung_total(transaksi)))
return "".join(baris)
print(buat_laporan([("Kopi", 15000), ("Gula", 12000), ("Teh", 8000)]), end="")Output yang Dihasilkan:
Kopi 15000
Gula 12000
Teh 8000
Total 35000Versi sesudah punya tiga function yang masing-masing bisa dites terpisah, dan buat_laporan kini membaca seperti daftar langkah. Nama function menjadi penjelas yang lebih kuat daripada komentar.
Angka Ajaib menjadi Konstanta #
Angka telanjang (500000, 0.10) memaksa pembaca menebak asalnya. Beri nama di tingkat module:
BATAS_DISKON = 500_000
TARIF_DISKON = 0.10
total_belanja = 600_000
diskon = total_belanja * TARIF_DISKON if total_belanja > BATAS_DISKON else 0
print(f"Diskon: {diskon:.0f}")Output yang Dihasilkan:
Diskon: 60000Rantai if/elif menjadi Dictionary #
Pemetaan kota ke ongkos kirim sering ditulis sebagai tangga if/elif yang terus bertambah:
def ongkos_kirim(kota):
if kota == "Jakarta":
return 15000
elif kota == "Bandung":
return 20000
elif kota == "Surabaya":
return 25000
else:
return 40000Ganti dengan dictionary — menambah kota baru berarti menambah satu baris data, bukan satu cabang logika:
ONGKOS_KIRIM = {
"Jakarta": 15_000,
"Bandung": 20_000,
"Surabaya": 25_000,
}
def ongkos_kirim(kota):
"""Ongkos kirim default untuk kota yang tidak terdaftar."""
return ONGKOS_KIRIM.get(kota, 40_000)
for kota in ["Jakarta", "Bandung", "Surabaya", "Medan"]:
print(f"{kota}: Rp{ongkos_kirim(kota):,}")Output yang Dihasilkan:
Jakarta: Rp15,000
Bandung: Rp20,000
Surabaya: Rp25,000
Medan: Rp40,000Guard Clauses #
Validasi yang bersarang if di dalam if membuat kode utama tergantung dalam. Balik logikanya: tolak kasus tidak sah lebih dulu, sisakan jalan bahagia tanpa indentasi:
def kirim_sewa(pelanggan):
"""Kirim tagihan sewa hanya ke pelanggan aktif ber-email."""
if pelanggan is None:
return False
if not pelanggan.get("email"):
return False
if pelanggan.get("status") != "aktif":
return False
print(f"Tagihan sewa dikirim ke {pelanggan['email']}")
return True
budi = {"email": "[email protected]", "status": "aktif"}
siti = {"email": "", "status": "aktif"}
print(kirim_sewa(budi))
print(kirim_sewa(siti))
print(kirim_sewa(None))Output yang Dihasilkan:
Tagihan sewa dikirim ke [email protected]
True
False
FalseDua refactor kecil lainnya cukup diringkas: inline variable menghapus variabel perantara yang dipakai sekali (harga_akhir = harga_asli - potongan langsung dipakai), dan rename mengganti singkatan tak jelas (byr(t, m)) menjadi nama yang menjelaskan (harga_total(tarif, meter)).
Katalog Code Smell #
Code smell adalah tanda di permukaan kode bahwa struktur di bawahnya bermasalah. Empat yang paling sering dijumpai:
| Code smell | Cirinya | Penawar |
|---|---|---|
| Long function | >20 baris, komentar penanda bagian | ekstrak function |
| Duplicated code | blok serupa di beberapa tempat (melanggar DRY: don’t repeat yourself) | ekstrak lalu panggil ulang |
| Long parameter list | 5+ parameter berjalan bersama | bungkus jadi dataclass |
| Feature envy | method lebih akrab dengan data class lain | pindahkan method ke class yang diminatinya |
Ritme Kerja yang Aman #
Semua contoh di atas menempuh langkah kecil: satu function diekstrak, satu cabang dibalik, satu angka diberi nama. Setelah tiap langkah, test dijalankan ulang. Bila merah, penyebabnya pasti langkah terakhir — lingkar kesalahan sempit itulah yang membuat refactoring berani dilakukan pada Jumat sore sekalipun. Kombinasikan dengan mypy dari Chapter Type Hints untuk Maintainability, dan perubahan signature pun ikut terjaga.