Kode yang bekerja bukan berarti kode yang sehat. Method raksasa dengan loop bersarang, variabel bernama tmp2, duplikasi blok di tiga tempat — semuanya berjalan benar hari ini, tetapi setiap perubahan berikutnya menjadi semakin mahal dan semakin berisiko. Refactoring adalah disiplin memperbaiki struktur internal kode tanpa mengubah perilaku eksternalnya: keluaran program, kontrak method, dan perilaku yang diamati pengguna tetap sama. Kata kuncinya adalah langkah kecil yang aman — bukan penulisan ulang besar-besaran yang menukar satu jenis risiko dengan risiko lain. Pada Chapter ini kita akan mengenal teknik-teknik dasarnya, peran test sebagai jaring pengaman, dan membuktikan dengan program bahwa refactoring sungguhan mempertahankan perilaku.
Perubahan Struktur, Bukan Perilaku #
Definisi refactoring perlu dipegang teguh karena membedakannya dari dua pekerjaan lain. Refactoring bukan menambah fitur — itu perubahan fungsional; dan bukan pula optimasi performa — itu mengubah karakteristik non-fungsional. Refactoring murni menata: nama lebih jelas, tanggung jawab lebih terpisah, duplikasi diangkat, kompleksitas diturunkan, semuanya sambil perilaku diamati dijaga identik.
Mengapa struktur penting jika program jalan? Karena waktu pembacaan kode jauh melebihi waktu penulisannya: fitur berikutnya, review, dan investigasi bug semuanya membaca. Struktur yang baik memperpendek jarak antara “membaca” dan “memahami”; struktur yang buruk memaksa setiap pembaca mengurai ulang logika yang sama. Refactoring adalah investasi pada semua pembaca di masa depan, termasuk diri kita sendiri enam bulan ke depan.
Teknik Dasar #
Katalog teknik refactoring (dipopulerkan Martin Fowler) berisi puluhan pola, tetapi empat berikut menutup sebagian besar kebutuhan harian:
| Teknik | Apa yang dilakukan | Masalah yang diselesaikan |
|---|---|---|
| Extract Method | Memindahkan blok kode menjadi method bernama | Method panjang; logika tak bernama |
| Rename Variable/Method | Mengganti nama menjadi deskriptif | Nama seperti tmp, data2, proses |
| Inline Variable/Method | Melebur variabel/method sebaris ke pemakainya | Indireksi yang tak lagi berguna |
| Move Method/Class | Memindahkan ke class/package yang lebih tepat | Tanggung jawab yang salah tempat |
Teknik paling sering dipakai adalah extract dan rename — dan keduanya paling terasa dampaknya karena menyentuh keterbacaan langsung. Satu prinsip mengikat semuanya: satu langkah kecil per perubahan, jalankan test setelahnya, baru lanjut ke langkah berikutnya.
Bukti Refactoring yang Aman #
Mari kita buktikan definisi “perilaku tetap” secara harfiah. Program berikut menyimpan dua versi logika yang sama — versi lama yang monolitik dan versi baru hasil extract method — menangkap keluaran keduanya ke StringBuilder, lalu membandingkannya:
import java.util.List;
public class BuktiRefactoring {
public static void main(String[] args) {
List<String> keranjang = List.of("Buku", "Pena", "Penghapus");
int[] harga = {25000, 5000, 3000};
StringBuilder jejakLama = new StringBuilder();
versiLama(keranjang, harga, jejakLama);
StringBuilder jejakBaru = new StringBuilder();
versiBaru(keranjang, harga, jejakBaru);
System.out.println("== Versi lama ==");
System.out.print(jejakLama);
System.out.println("== Versi baru ==");
System.out.print(jejakBaru);
System.out.println("Perilaku identik: "
+ jejakLama.toString().equals(jejakBaru.toString()));
}
// Versi lama: satu blok panjang, banyak tanggung jawab sekaligus
static void versiLama(List<String> keranjang, int[] harga, StringBuilder jejak) {
int total = 0;
for (int i = 0; i < keranjang.size(); i++) {
total += harga[i];
}
double diskon = 0;
if (total >= 30000) {
diskon = total * 0.1;
}
jejak.append("item: ").append(keranjang.size()).append('\n');
jejak.append("total: ").append(total).append('\n');
jejak.append("diskon: ").append((int) diskon).append('\n');
jejak.append("bayar: ").append((int) (total - diskon)).append('\n');
}
// Versi baru: nama bermakna + method kecil dengan satu tanggung jawab
static void versiBaru(List<String> keranjang, int[] harga, StringBuilder jejak) {
int total = hitungTotal(harga);
int diskon = hitungDiskon(total);
jejak.append("item: ").append(keranjang.size()).append('\n');
jejak.append("total: ").append(total).append('\n');
jejak.append("diskon: ").append(diskon).append('\n');
jejak.append("bayar: ").append(total - diskon).append('\n');
}
static int hitungTotal(int[] harga) {
int total = 0;
for (int h : harga) {
total += h;
}
return total;
}
static int hitungDiskon(int total) {
if (total >= 30000) {
return (int) (total * 0.1);
}
return 0;
}
}Output yang Dihasilkan:
== Versi lama ==
item: 3
total: 33000
diskon: 3300
bayar: 29700
== Versi baru ==
item: 3
total: 33000
diskon: 3300
bayar: 29700
Perilaku identik: trueMari kita bedah langkah refactoring yang terjadi antara kedua versi:
- Extract
hitungTotal: loop penjumlahan harga — blok dengan tanggung jawab jelas — menjadi method bernama. Pemanggil kini membaca apa yang dihitung tanpa membaca bagaimana. - Extract
hitungDiskon: aturan bisnis diskon 10% di atas 30000 terisolasi di satu method — saat aturan berubah (misalnya berjenjang), hanya satu method yang disentuh. - Pemilihan tipe yang lebih tepat: variabel
double diskonpada versi lama digantiinthasil method — menghilangkan representasi pecahan untuk nilai rupiah utuh, sekaligus menyederhanakan pencetakan tanpa cast. - Baris terakhir keluaran
Perilaku identik: true: kedua jejak dibandingkan karakter demi karakter dan hasilnya sama persis — itulah definisi operasional refactoring yang aman. Dalam proyek nyata, pembanding ini adalah rangkaian unit test yang menjalankan kedua struktur melalui kontrak yang sama.
Perhatikan pula apa yang tidak berubah: urutan keluaran, format angka, nama field, dan hasil akhir 29700. Seandainya salah satu baris berbeda, perubahan itu bukan refactoring melainkan perubahan perilaku yang menuntut peninjauan terpisah.
Jaring Pengaman: Test dan IDE #
Refactoring tanpa jaring pengaman adalah pertaruhan. Jaring itu dua lapis. Pertama, test otomatis: sebelum menyentuh struktur, pastikan test yang ada menutup perilaku yang akan dijaga; bila belum, tulis test kecil untuk kontrak method yang hendak diubah lebih dulu. Kedua, otomasi IDE: IntelliJ IDEA, Eclipse, dan VS Code menjalankan rename dan extract sebagai transformasi berbasis pohon sintaks — seluruh lokasi pemakaian diperbarui sekaligus, termasuk pemanggilan lintas berkas, tanpa risiko find-and-replace yang menyeret nama serupa di komentar maupun string.
Siklus kerja refactoring yang sehat:
pastikan test hijau -> satu langkah refactoring -> jalankan test -> ulangiSiklus pendek tersebut membuat kesalahan tertangkap dalam hitungan detik, saat konteks perubahan masih segar di kepala. Bandingkan dengan menumpuk sepuluh langkah lalu menjalankan test: saat satu test gagal, penyebabnya bisa berada di langkah mana pun.
Struktur internal yang sehat menjaga kecepatan jangka panjang — tetapi tidak semua masalah struktur dapat ditunggu sampai “senggang”. Keputusan menunda perbaikan demi tenggat adalah kenyataan proyek, dan penundaan yang dibiarkan tanpa catatan mengendap menjadi utang teknis. Chapter berikutnya membahas cara berpikir tentang utang tersebut: kapan ia rasional, bagaimana bunganya bekerja, dan strategi mengelolanya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
BuktiRefactoring.java
Unduh berkasimport java.util.List;
public class BuktiRefactoring {
public static void main(String[] args) {
List<String> keranjang = List.of("Buku", "Pena", "Penghapus");
int[] harga = {25000, 5000, 3000};
StringBuilder jejakLama = new StringBuilder();
versiLama(keranjang, harga, jejakLama);
StringBuilder jejakBaru = new StringBuilder();
versiBaru(keranjang, harga, jejakBaru);
System.out.println("== Versi lama ==");
System.out.print(jejakLama);
System.out.println("== Versi baru ==");
System.out.print(jejakBaru);
System.out.println("Perilaku identik: "
+ jejakLama.toString().equals(jejakBaru.toString()));
}
// Versi lama: satu blok panjang, banyak tanggung jawab sekaligus
static void versiLama(List<String> keranjang, int[] harga, StringBuilder jejak) {
int total = 0;
for (int i = 0; i < keranjang.size(); i++) {
total += harga[i];
}
double diskon = 0;
if (total >= 30000) {
diskon = total * 0.1;
}
jejak.append("item: ").append(keranjang.size()).append('\n');
jejak.append("total: ").append(total).append('\n');
jejak.append("diskon: ").append((int) diskon).append('\n');
jejak.append("bayar: ").append((int) (total - diskon)).append('\n');
}
// Versi baru: nama bermakna + method kecil dengan satu tanggung jawab
static void versiBaru(List<String> keranjang, int[] harga, StringBuilder jejak) {
int total = hitungTotal(harga);
int diskon = hitungDiskon(total);
jejak.append("item: ").append(keranjang.size()).append('\n');
jejak.append("total: ").append(total).append('\n');
jejak.append("diskon: ").append(diskon).append('\n');
jejak.append("bayar: ").append(total - diskon).append('\n');
}
static int hitungTotal(int[] harga) {
int total = 0;
for (int h : harga) {
total += h;
}
return total;
}
static int hitungDiskon(int total) {
if (total >= 30000) {
return (int) (total * 0.1);
}
return 0;
}
}