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:

  1. Jalankan test — harus hijau semua.
  2. Ubah satu hal pada struktur kode.
  3. Jalankan test lagi — harus tetap hijau.
  4. 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:

python
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 teks

Ekstrak dua niatnya menjadi function bernama, lalu susun ulang:

python
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:

text
Kopi           15000
Gula           12000
Teh             8000
Total          35000

Versi 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:

python
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:

text
Diskon: 60000

Rantai if/elif menjadi Dictionary #

Pemetaan kota ke ongkos kirim sering ditulis sebagai tangga if/elif yang terus bertambah:

python
def ongkos_kirim(kota):
    if kota == "Jakarta":
        return 15000
    elif kota == "Bandung":
        return 20000
    elif kota == "Surabaya":
        return 25000
    else:
        return 40000

Ganti dengan dictionary — menambah kota baru berarti menambah satu baris data, bukan satu cabang logika:

python
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:

text
Jakarta: Rp15,000
Bandung: Rp20,000
Surabaya: Rp25,000
Medan: Rp40,000

Guard 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:

python
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:

text
Tagihan sewa dikirim ke [email protected]
True
False
False

Dua 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 smellCirinyaPenawar
Long function>20 baris, komentar penanda bagianekstrak function
Duplicated codeblok serupa di beberapa tempat (melanggar DRY: don’t repeat yourself)ekstrak lalu panggil ulang
Long parameter list5+ parameter berjalan bersamabungkus jadi dataclass
Feature envymethod lebih akrab dengan data class lainpindahkan 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.