Kode yang berjalan benar hari ini belum tentu mudah diubah besok. Seiring fitur bertambah, fungsi memanjang, nama kehilangan makna, dan logika yang tadinya sederhana perlahan berubah menjadi kode yang ditakuti siapa pun yang menyentuhnya. Refactoring adalah disiplin menata ulang kode semacam itu tanpa mengubah perilakunya, sehingga setiap perubahan berikutnya justru semakin murah. Pada chapter ini kita mempelajari alur kerja refactoring yang aman: mengunci perilaku dengan tes terlebih dahulu, melangkah sekecil mungkin, dan menanamkan setiap langkah sebagai commit tersendiri.

Apa Itu Refactoring #

Refactoring adalah proses mengubah struktur internal kode tanpa mengubah perilaku eksternalnya. Perilaku eksternal berarti: untuk input yang sama, program menghasilkan output dan efek samping yang persis sama seperti sebelum ditata ulang. Yang berubah hanyalah cara kode mengatur dirinya sendiri — pemenggalan fungsi, penamaan, hingga letak logika antar class.

Dengan definisi tersebut, refactoring berbeda dari tiga aktivitas lain yang sering tertukar. Menambah fitur jelas mengubah perilaku, sehingga bukan refactoring. Optimasi performa mengubah karakteristik non-fungsional seperti waktu tempuh dan pemakaian memori, dengan sasaran yang berbeda dari keterbacaan. Adapun menulis ulang (rewrite) mengganti implementasi dari nol sambil bertaruh versi baru akan lebih baik, tanpa jaminan perilaku identik. Refactoring bekerja bertolak belakang dari rewrite: struktur boleh berubah total, tetapi dilakukan sedikit demi sedikit dan perilaku dijaga tetap identik di setiap langkah.

Jaring Pengaman Terlebih Dahulu #

Menata ulang kode tanpa tes seperti membedah mesin tanpa alat ukur: kita tidak dapat membuktikan apa pun setelahnya. Aturan pertama refactoring karena itu adalah menyiapkan jaring pengaman berupa tes yang mengunci perilaku saat ini. Kita sudah mempelajari cara menulis tes berbasis assertion pada chapter Unit Testing ; pola yang sama kita pakai di sini.

Cara kerjanya sebagai berikut. Sebelum menyentuh kode, tulis assertion yang merekam output fungsi untuk beberapa skenario, terutama kasus batas. Jalankan assertion terhadap kode lama dan pastikan semuanya lolos. Setelah itu lakukan penataan ulang, lalu jalankan assertion yang sama sekali lagi. Selama hasilnya tetap lolos, kita punya bukti nyata bahwa perilaku tidak berubah — bukan sekadar perasaan “sepertinya sama”.

Pola Ekstraksi Method #

Ekstraksi method (extract method) adalah refactoring yang paling sering dipakai: mengangkat sekelompok baris menjadi fungsi baru dengan nama deskriptif. Kita mulai dari fungsi berikut yang masih bekerja dengan benar, tetapi menumpuk tiga keputusan bisnis dalam satu tubuh: perhitungan subtotal, aturan diskon member, dan ambang gratis ongkir.

php
<?php
function tagihanLama(array $pesanan): string
{
    $subtotal = 0;
    foreach ($pesanan["items"] as $item) {
        $subtotal += $item["harga"] * $item["jumlah"];
    }

    $diskon = $pesanan["member"] ? intdiv($subtotal, 10) : 0;
    $ongkir = $subtotal >= 100000 ? 0 : 15000;
    $total = $subtotal - $diskon + $ongkir;

    return sprintf(
        "Subtotal: Rp%d | Diskon: Rp%d | Ongkir: Rp%d | Total: Rp%d",
        $subtotal,
        $diskon,
        $ongkir,
        $total
    );
}

$pesanan = [
    "member" => true,
    "items" => [
        ["nama" => "Kursus PHP Dasar", "harga" => 95000, "jumlah" => 1],
        ["nama" => "Buku Referensi PHP", "harga" => 45000, "jumlah" => 2],
    ],
];

echo tagihanLama($pesanan), "\n";

Output yang Dihasilkan:

text
Subtotal: Rp185000 | Diskon: Rp18500 | Ongkir: Rp0 | Total: Rp166500

Sekarang kita tata ulang. Perilaku fungsi di atas dikunci lebih dulu dengan dua skenario batas — member di bawah ambang ongkir dan non-member di bawah ambang — lalu setiap keputusan bisnis diangkat menjadi fungsi kecil bernama deskriptif:

php
<?php
function hitungSubtotal(array $items): int
{
    $subtotal = 0;
    foreach ($items as $item) {
        $subtotal += $item["harga"] * $item["jumlah"];
    }
    return $subtotal;
}

function hitungDiskon(int $subtotal, bool $member): int
{
    return $member ? intdiv($subtotal, 10) : 0;
}

function hitungOngkir(int $subtotal): int
{
    return $subtotal >= 100000 ? 0 : 15000;
}

function tagihan(array $pesanan): string
{
    $subtotal = hitungSubtotal($pesanan["items"]);
    $diskon = hitungDiskon($subtotal, $pesanan["member"]);
    $ongkir = hitungOngkir($subtotal);
    $total = $subtotal - $diskon + $ongkir;

    return sprintf(
        "Subtotal: Rp%d | Diskon: Rp%d | Ongkir: Rp%d | Total: Rp%d",
        $subtotal,
        $diskon,
        $ongkir,
        $total
    );
}

function periksaSama(mixed $nyata, mixed $diharapkan, string $label): void
{
    echo $label, ": ", $nyata === $diharapkan ? "lolos" : "GAGAL", "\n";
}

$memberBesar = [
    "member" => true,
    "items" => [["nama" => "Kursus PHP Dasar", "harga" => 95000, "jumlah" => 1]],
];
$nonMemberKecil = [
    "member" => false,
    "items" => [["nama" => "Stiker", "harga" => 15000, "jumlah" => 2]],
];

periksaSama(tagihan($memberBesar), "Subtotal: Rp95000 | Diskon: Rp9500 | Ongkir: Rp15000 | Total: Rp100500", "member di bawah minimum ongkir");
periksaSama(tagihan($nonMemberKecil), "Subtotal: Rp30000 | Diskon: Rp0 | Ongkir: Rp15000 | Total: Rp45000", "non-member di bawah minimum ongkir");

$pesanan = [
    "member" => true,
    "items" => [
        ["nama" => "Kursus PHP Dasar", "harga" => 95000, "jumlah" => 1],
        ["nama" => "Buku Referensi PHP", "harga" => 45000, "jumlah" => 2],
    ],
];
echo tagihan($pesanan), "\n";

Output yang Dihasilkan:

text
member di bawah minimum ongkir: lolos
non-member di bawah minimum ongkir: lolos
Subtotal: Rp185000 | Diskon: Rp18500 | Ongkir: Rp0 | Total: Rp166500

Mari kita telaah hasil penataan ulang tersebut:

  1. hitungSubtotal(): seluruh perulungan penjumlahan pindah ke sini. Fungsi ini menerima daftar item dan mengembalikan subtotal, tanpa tahu apa pun soal diskon atau ongkir.
  2. hitungDiskon(): aturan “member mendapat sepersepuluh” terbungkus dalam satu baris return. Ketika aturannya berubah menjadi berjenjang, hanya fungsi ini yang disentuh.
  3. hitungOngkir(): ambang 100000 dan tarif 15000 kini punya rumah bernama jelas, bukan angka asing di tengah fungsi besar.
  4. tagihan(): tubuh fungsinya sekarang membaca seperti daftar langkah — subtotal, diskon, ongkir, total. Pembaca tidak perlu memilah matematika untuk mengerti alurnya.
  5. periksaSama(): dua baris terakhir membuktikan dua hal sekaligus — kedua assertion lolos, dan output untuk $pesanan yang sama persis dengan output tagihanLama() pada blok sebelumnya. Perilaku terjaga, struktur membaik.

Katalog Refactoring Mini #

Refactoring yang terbukti aman sudah dikatalogkan dan diberi nama, sehingga tim dapat berdiskusi dengan kosakata yang sama. Tabel berikut merangkum beberapa yang paling sering terpakai sehari-hari:

RefactoringPemicu umumTindakan
Rename Variable / Methodnama berbohong tentang isiganti nama dan perbarui seluruh referensi
Extract Methodfungsi panjang, blok butuh komentarangkat blok menjadi fungsi bernama deskriptif
Inline Methodisi fungsi lebih jelas daripada namanyakembalikan tubuhnya ke pemanggil
Move Methodfungsi lebih sering memakai data class lainpindahkan fungsi ke class pemilik data tersebut
Replace Magic Number with Constantangka tanpa nama berserakanangkat menjadi konstanta bernama
Replace Conditional with Polymorphismmatch pada jenis yang sama berulang di banyak tempatjadikan setiap cabang class tersendiri

Pratinjau Replace Conditional dengan Polymorphism #

Refactoring terakhir pada tabel di atas layak diperlihatkan karena sering menjadi gerbang menuju pemahaman pola desain. Mulai dari fungsi ongkir berbasis match berikut:

php
<?php
function ongkirKondisional(string $kurir, int $beratGram): int
{
    return match ($kurir) {
        "ekonomi" => 6000 + intdiv($beratGram, 1000) * 1500,
        "reguler" => 10000 + intdiv($beratGram, 1000) * 2000,
        "ekspres" => 25000 + intdiv($beratGram, 1000) * 4000,
    };
}

foreach (["ekonomi", "reguler", "ekspres"] as $kurir) {
    echo strtoupper($kurir), " 2500 gram: Rp", ongkirKondisional($kurir, 2500), "\n";
}

Output yang Dihasilkan:

text
EKONOMI 2500 gram: Rp9000
REGULER 2500 gram: Rp14000
EKSPRES 2500 gram: Rp33000

Versi berikut memindahkan setiap cabang match menjadi class yang mengimplementasikan interface Kurir. Menambah kurir baru tidak lagi menyentuh kode yang ada — cukup menulis class baru, prinsip yang kita bahas tuntas pada chapter Strategy Pattern :

php
<?php
interface Kurir
{
    public function ongkir(int $beratGram): int;
}

final class KurirEkonomi implements Kurir
{
    public function ongkir(int $beratGram): int
    {
        return 6000 + intdiv($beratGram, 1000) * 1500;
    }
}

final class KurirReguler implements Kurir
{
    public function ongkir(int $beratGram): int
    {
        return 10000 + intdiv($beratGram, 1000) * 2000;
    }
}

final class KurirEkspres implements Kurir
{
    public function ongkir(int $beratGram): int
    {
        return 25000 + intdiv($beratGram, 1000) * 4000;
    }
}

$layanan = [
    "ekonomi" => new KurirEkonomi(),
    "reguler" => new KurirReguler(),
    "ekspres" => new KurirEkspres(),
];

foreach (["ekonomi", "reguler", "ekspres"] as $nama) {
    echo strtoupper($nama), " 2500 gram: Rp", $layanan[$nama]->ongkir(2500), "\n";
}

Output yang Dihasilkan:

text
EKONOMI 2500 gram: Rp9000
REGULER 2500 gram: Rp14000
EKSPRES 2500 gram: Rp33000

Output kedua versi identik baris demi baris — bukti bahwa perpindahan dari kondisional ke polymorphism memang refactoring, bukan perubahan fitur. Pemanggil tidak lagi memegang string jenis kurir dan tabel tarifnya; pemanggil cukup memegang object yang tahu cara menghitung ongkirnya sendiri.

Langkah Kecil dan Commit per Langkah #

Refactoring yang aman disusun dari langkah-langkah kecil yang masing-masing meninggalkan kode dalam keadaan hijau: tes lolos dan program tetap berjalan. Pola kerjanya selalu sama — buat satu perubahan struktural, jalankan tes, commit. Ulangi. Riwayat commit berikut memperlihatkan bentuk akhirnya:

text
$ git log --oneline
a1f2c3d (HEAD -> main) refactor: ganti conditional ongkir dengan polymorphism
9e8d7c6 refactor: ekstrak hitungOngkir() dari tagihan()
3b4a5f6 refactor: ganti nama $d menjadi $diskon
7c2e1f0 fitur: hitung diskon member 10%

Perhatikan bahwa commit fitur dan commit refactoring tidak pernah bercampur. Ketika sebuah langkah ternyata salah, git revert pada satu commit kecil langsung membatalkan langkah tersebut tanpa menyeret perubahan lain. Bayangkan jika seluruh baris di atas jadi satu commit raksasa: satu kesalahan kecil memaksa kita memilah ulang semuanya secara manual.

Code Smells sebagai Pemicu #

Dari mana kita tahu kode butuh dirapikan? Sinyalnya disebut code smell — bau yang tidak selalu berarti bug, tetapi menandakan tempat yang rawan membusuk. Tabel berikut memetakan bau paling umum dengan refactoring penangkalnya:

Code smellGejalaRefactoring penangkal
Long methodfungsi melebihi kira-kira 40 baris atau butuh komentar seksiExtract Method
Duplicated codelogika sama muncul di dua tempat atau lebihExtract Method lalu berbagi fungsi
Magic numberangka seperti 0.1 atau 15000 tanpa namaReplace Magic Number with Constant
Long parameter listlebih dari empat parameter yang selalu berkelompokbungkus menjadi object parameter
Nested conditionalsif di dalam if di dalam ifguard clause / early return
Feature envyfungsi lebih sering membaca data milik class lainMove Method
Comments as deodorantkomentar dipakai menutupi blok yang membingungkanExtract Method dengan nama deskriptif

Peran IDE dan Analisis Statis #

Sebagian besar refactoring pada katalog di atas sudah dibantu penuh oleh IDE. Operasi rename symbol memperbarui seluruh referensi lintas berkas berdasarkan pemahaman struktur kode, bukan pencocokan teks. Operasi extract method menawarkan kandidat parameter dan return value secara otomatis. Alat analisis statis seperti PHPStan menjaga jenis selama proses penataan berlangsung, sementara PHP CS Fixer merapikan gaya tanpa perlu campur tangan manusia.

Ringkasan #

Refactoring menata ulang struktur kode sambil menjaga perilaku tetap identik, dan kuncinya ada pada urutan kerja: kunci perilaku dengan tes, ekstrak langkah kecil, jalankan tes, commit, ulangi. Code smell memberi tahu kita mulai dari mana, katalog refactoring memberi tahu apa yang dilakukan, dan IDE membuat semuanya cepat serta aman. Namun penataan ulang yang sudah rapi tetap perlu diperiksa mata kedua sebelum menyatu ke kode utama — pada chapter berikutnya kita bahas proses tersebut: code review.

Referensi #