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:

TeknikApa yang dilakukanMasalah yang diselesaikan
Extract MethodMemindahkan blok kode menjadi method bernamaMethod panjang; logika tak bernama
Rename Variable/MethodMengganti nama menjadi deskriptifNama seperti tmp, data2, proses
Inline Variable/MethodMelebur variabel/method sebaris ke pemakainyaIndireksi yang tak lagi berguna
Move Method/ClassMemindahkan ke class/package yang lebih tepatTanggung 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:

text
== Versi lama ==
item: 3
total: 33000
diskon: 3300
bayar: 29700
== Versi baru ==
item: 3
total: 33000
diskon: 3300
bayar: 29700
Perilaku identik: true

Mari kita bedah langkah refactoring yang terjadi antara kedua versi:

  1. Extract hitungTotal: loop penjumlahan harga — blok dengan tanggung jawab jelas — menjadi method bernama. Pemanggil kini membaca apa yang dihitung tanpa membaca bagaimana.
  2. Extract hitungDiskon: aturan bisnis diskon 10% di atas 30000 terisolasi di satu method — saat aturan berubah (misalnya berjenjang), hanya satu method yang disentuh.
  3. Pemilihan tipe yang lebih tepat: variabel double diskon pada versi lama diganti int hasil method — menghilangkan representasi pecahan untuk nilai rupiah utuh, sekaligus menyederhanakan pencetakan tanpa cast.
  4. 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.

text
Siklus kerja refactoring yang sehat:
  pastikan test hijau -> satu langkah refactoring -> jalankan test -> ulangi

Siklus 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 berkas
java
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;
    }
}