Seluruh section ini kita pakai untuk memberi setiap bagian program rumah yang tepat — modul, lapisan, prinsip, pola, antarmuka, dan kebijakan resource. Namun kode yang berjalan di dunia nyata tidak pernah ditulis sekali jadi dalam bentuk akhirnya: kebutuhan datang bertahap, batas waktu menekan, dan struktur yang semula rapi perlahan tertimbun. Refactoring adalah keterampilan mengubah struktur kode tanpa mengubah perilakunya — kemampuan yang membuat seluruh bekal desain section ini bisa diterapkan pada kode yang sudah ada, bukan hanya pada kode yang baru mulai ditulis. Chapter ini membedah empat code smell yang paling sering muncul, langkah aman untuk menanganinya, dan satu refactor utuh yang sebelum-sesudahnya dibuktikan menghasilkan output yang sama persis.
Mengubah Struktur, Menjaga Perilaku #
Definisinya perlu dipisah tegas dari dua pekerjaan lain. Refactoring bukan perbaikan bug — perilaku lama yang salah tetap salah selama refactor berjalan; perbaikan logika adalah perubahan terpisah yang dilakukan setelah struktur beres. Refactoring juga bukan penambahan fitur — kemampuan baru ditulis setelah kode tujuan berada dalam bentuk yang bisa diterima. Pemisahan ini bukan sekadar tata kelola: dua perubahan sekaligus memaksa reviewer membedah dua hal dalam satu diff, dan saat hasil melenceng, tidak ada cara cepat menentukan mana yang menjadi penyebabnya.
Waktu yang tepat untuk refactor biasanya melekat pada pekerjaan lain: sebelum menambah fitur ke sebuah function (“buat rumahnya rapi dulu, terus mudah menambah perabot”), saat review menemukan struktur yang menghambat, dan sebagai langkah penguat setelah test hijau. Satu aturan yang berlaku di semua kasus: refactoring dijalankan dalam langkah kecil yang masing-masing bisa dikompilasi dan diuji — bukan sebagai satu perombakan besar yang hanya diuji di ujung jalan.
Code Smell: Empat Gejala yang Paling Sering Muncul #
Code smell adalah gejala di permukaan kode yang sering menandai masalah struktur lebih dalam — bukan kesalahan yang harus diadili, melainkan petunjuk arah yang memberi tahu ke mana perlu melihat. Empat yang paling sering kita temui:
| Code smell | Gejala di kode | Obat yang lazim |
|---|---|---|
| Fungsi panjang | Satu function puluhan baris dengan banyak blok komentar sebagai “bab” | Extract function per blok |
| Duplikasi | Formula atau blok yang sama tertulis di dua tempat atau lebih | Extract function, satu rumah untuk satu aturan |
| Parameter panjang | Daftar parameter lima ke atas, tipe serupa berjajar | Parameter object (struct) |
| Primitive obsession | Konsep domain dipecah ke tipe mentah (int untuk level, string bebas untuk status) | Tipe khusus: enum class, struct |
Gejala-gejala ini saling menarik: fungsi panjang hampir selalu memuat duplikasi, dan parameter panjang muncul karena data yang berubah bersama belum dikemas. Berkas berikut memuat keempatnya sekaligus pada satu tugas yang akrab — menghitung gaji harian dengan lembur, tunjangan, dan potongan:
// refactor-sebelum.cpp — SEBELUM: fungsi panjang, duplikasi, parameter panjang, magic number
// Kompilasi: g++ -std=c++23 -Wall -Wextra refactor-sebelum.cpp -o refactor-sebelum
#include <print>
#include <string>
// level: 1 = Junior, 2 = Menengah, 3 = Senior — maknanya hanya hidup di komentar ini
int totalGajiBersih(int jamNormal, int jamLembur, int tarif, int level, bool asuransi) {
int upah = jamNormal * tarif;
int lembur = 0;
if (jamLembur > 0) {
if (level == 3) {
lembur = jamLembur * tarif * 2; // senior: 2x tarif
} else {
lembur = jamLembur * tarif * 3 / 2; // level lain: 1,5x tarif
}
}
int tunjangan = level * 500000;
int bruto = upah + lembur + tunjangan;
int potongan = bruto * 5 / 100 + (asuransi ? 30000 : 0);
return bruto - potongan;
}
void cetakSlipGaji(std::string nama, int jamNormal, int jamLembur,
int tarif, int level, bool asuransi) {
// perhitungan yang SAMA dengan totalGajiBersih di atas — duplikasi utuh
int upah = jamNormal * tarif;
int lembur = 0;
if (jamLembur > 0) {
if (level == 3) {
lembur = jamLembur * tarif * 2;
} else {
lembur = jamLembur * tarif * 3 / 2;
}
}
int tunjangan = level * 500000;
int bruto = upah + lembur + tunjangan;
int potongan = bruto * 5 / 100 + (asuransi ? 30000 : 0);
std::println("--- {} ---", nama);
std::println("upah normal : Rp{}", upah);
std::println("upah lembur : Rp{}", lembur);
std::println("tunjangan : Rp{}", tunjangan);
std::println("gaji kotor : Rp{}", bruto);
std::println("potongan : Rp{}", potongan);
std::println("gaji bersih : Rp{}", bruto - potongan);
}
static int lulus = 0;
static int gagal = 0;
void cek(bool kondisi, const std::string& deskripsi) {
if (kondisi) {
++lulus;
std::println("[ LULUS ] {}", deskripsi);
} else {
++gagal;
std::println("[ GAGAL ] {}", deskripsi);
}
}
int main() {
std::println("== slip gaji ==");
cetakSlipGaji("Budi", 160, 10, 25000, 3, true); // 3 itu level? true yang mana?
cetakSlipGaji("Siti", 152, 0, 25000, 2, false);
cetakSlipGaji("Dewi", 80, 12, 20000, 1, true);
std::println("== rekap bulan ini ==");
std::println("{:<5}: Rp{}", "Budi", totalGajiBersih(160, 10, 25000, 3, true));
std::println("{:<5}: Rp{}", "Siti", totalGajiBersih(152, 0, 25000, 2, false));
std::println("{:<5}: Rp{}", "Dewi", totalGajiBersih(80, 12, 20000, 1, true));
int total = totalGajiBersih(160, 10, 25000, 3, true)
+ totalGajiBersih(152, 0, 25000, 2, false)
+ totalGajiBersih(80, 12, 20000, 1, true);
std::println("total: Rp{}", total);
std::println("== kunci perilaku ==");
cek(totalGajiBersih(160, 10, 25000, 3, true) == 5670000, "gaji bersih Budi = 5670000");
cek(totalGajiBersih(152, 0, 25000, 2, false) == 4560000, "gaji bersih Siti = 4560000");
cek(totalGajiBersih(80, 12, 20000, 1, true) == 2307000, "gaji bersih Dewi = 2307000");
std::println("ringkasan: {} lulus, {} gagal", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
== slip gaji ==
--- Budi ---
upah normal : Rp4000000
upah lembur : Rp500000
tunjangan : Rp1500000
gaji kotor : Rp6000000
potongan : Rp330000
gaji bersih : Rp5670000
--- Siti ---
upah normal : Rp3800000
upah lembur : Rp0
tunjangan : Rp1000000
gaji kotor : Rp4800000
potongan : Rp240000
gaji bersih : Rp4560000
--- Dewi ---
upah normal : Rp1600000
upah lembur : Rp360000
tunjangan : Rp500000
gaji kotor : Rp2460000
potongan : Rp153000
gaji bersih : Rp2307000
== rekap bulan ini ==
Budi : Rp5670000
Siti : Rp4560000
Dewi : Rp2307000
total: Rp12537000
== kunci perilaku ==
[ LULUS ] gaji bersih Budi = 5670000
[ LULUS ] gaji bersih Siti = 4560000
[ LULUS ] gaji bersih Dewi = 2307000
ringkasan: 3 lulus, 0 gagalKeempat smell-nya bisa ditunjuk di lokasi masing-masing. Duplikasi paling gamblang: aturan lembur, tunjangan, dan potongan tertulis utuh dua kali — di totalGajiBersih (baris 8-22) dan lagi di cetakSlipGaji (baris 24-38); ketika aturan pajak berubah dari 5% ke 6%, dua tempat harus diingat, dan yang terlewat adalah bug senyap. Fungsi panjang: cetakSlipGaji menghitung sekaligus mencetak, dua tanggung jawab dalam satu badan. Parameter panjang: enam parameter dengan tipe int yang berjajar — cetakSlipGaji("Budi", 160, 10, 25000, 3, true) memaksa pembaca menebak arti setiap angka. Primitive obsession: level dinyatakan sebagai int biasa dengan makna yang hanya hidup di komentar, dan 500000, 5, 30000 adalah magic number tanpa nama. Versi sesudah nanti menunjukkan bahwa sebagian smell tidak diobati satu per satu — mengemas data yang benar sering kali menghilangkan dua gejala sekaligus.
Kunci Perilaku Sebelum Mengubah #
Langkah pertama refactoring sebenarnya terjadi sebelum satu baris pun diubah: pastikan perilaku saat ini terkunci oleh test. Program di atas sudah memuatnya — bagian == kunci perilaku == memeriksa tiga nilai gaji yang diketahui dan keluar dengan exit code 1 bila ada yang meleset, pola harness yang dibangun di chapter Unit Testing. Di proyek nyata, cek-cek itu tinggal di test suite yang dijalankan ulang setiap langkah; di sini ia ditempatkan dalam satu file agar bisa kita jalankan bersama. Tiga nilai itu bukan angka sembarang — mereka hasil eksekusi versi sebelum, dan menjadi patokan bahwa versi sesudah harus menghasilkan nilai yang sama.
Kaidahnya sederhana: tidak ada refactor di atas kode yang tidak teruji. Bila test-nya belum ada, tulis dulu dengan memperlakukan kode lama sebagai spesifikasi — apa pun yang dikerjakannya hari ini dianggap benar. Setelah test hijau, kita bebas memindahkan struktur sepuasnya, selama test tetap hijau.
Langkah Refactor: Kecil, Berurutan, Selalu Hijau #
Dari versi sebelum ke sesudah, langkahnya enam dan masing-masing bisa dikompilasi serta diuji sebelum lanjut:
- Tulis test pengunci perilaku — tiga cek nilai gaji di atas; jalankan, pastikan hijau.
- Beri nama magic number —
500000,30000,5menjadiconstexpr intbernama; perilaku tidak berubah, makna naik ke permukaan. - Obati primitive obsession —
int levelmenjadienum class Level { Junior = 1, Menengah = 2, Senior = 3 }; nilai eksplisit menjaga pemetaan lama tetap sama. - Kemas parameter panjang menjadi parameter object — lima parameter perhitungan menjadi
struct RincianKerja, dan call site memakai designated initializer yang menyebut nama. - Extract function per aturan bisnis — lembur, tunjangan, gaji kotor, potongan, dan gaji bersih masing-masing dapat satu function; duplikasi hilang karena aturan kini punya satu rumah.
- Pisahkan presentasi —
cetakSliptinggal menampilkan hasil function perhitungan, mengikuti pemisahan lapisan dari chapter Separation of Concerns.
Setiap langkah diakhiri kompilasi plus test — hijau berarti langkah aman dan boleh lanjut, merah berarti langkah terakhir dikembalikan sebelum salah paham menumpuk.
Versi sesudahnya:
// refactor-sesudah.cpp — SESUDAH: struktur diperbaiki, perilaku dan output tidak berubah
// Kompilasi: g++ -std=c++23 -Wall -Wextra refactor-sesudah.cpp -o refactor-sesudah
#include <print>
#include <string>
#include <utility>
#include <vector>
// ===== smell primitive obsession diobati: level jadi tipe yang tak bisa keliru =====
enum class Level { Junior = 1, Menengah = 2, Senior = 3 };
// ===== smell parameter panjang diobati: data yang berubah bersama jadi satu object =====
struct RincianKerja {
int jamNormal;
int jamLembur;
int tarifPerJam;
Level level;
bool ikutAsuransi;
};
// ===== magic number diberi nama: aturan bisnis terbaca langsung =====
constexpr int TUNJANGAN_PER_LEVEL = 500000;
constexpr int IURAN_ASURANSI = 30000;
constexpr int PAJAK_PERSEN = 5;
// ===== duplikasi diobati: setiap aturan bisnis punya satu rumah =====
int upahLembur(const RincianKerja& r) {
if (r.jamLembur <= 0) {
return 0;
}
if (r.level == Level::Senior) {
return r.jamLembur * r.tarifPerJam * 2; // senior: 2x tarif
}
return r.jamLembur * r.tarifPerJam * 3 / 2; // level lain: 1,5x tarif
}
int tunjangan(const RincianKerja& r) {
return static_cast<int>(r.level) * TUNJANGAN_PER_LEVEL;
}
int gajiKotor(const RincianKerja& r) {
int upah = r.jamNormal * r.tarifPerJam;
return upah + upahLembur(r) + tunjangan(r);
}
int potongan(const RincianKerja& r) {
int pajak = gajiKotor(r) * PAJAK_PERSEN / 100;
int iuran = r.ikutAsuransi ? IURAN_ASURANSI : 0;
return pajak + iuran;
}
int gajiBersih(const RincianKerja& r) {
return gajiKotor(r) - potongan(r);
}
// ===== fungsi panjang diobati: presentasi berdiri sendiri tanpa perhitungan =====
void cetakSlip(const std::string& nama, const RincianKerja& r) {
std::println("--- {} ---", nama);
std::println("upah normal : Rp{}", r.jamNormal * r.tarifPerJam);
std::println("upah lembur : Rp{}", upahLembur(r));
std::println("tunjangan : Rp{}", tunjangan(r));
std::println("gaji kotor : Rp{}", gajiKotor(r));
std::println("potongan : Rp{}", potongan(r));
std::println("gaji bersih : Rp{}", gajiBersih(r));
}
static int lulus = 0;
static int gagal = 0;
void cek(bool kondisi, const std::string& deskripsi) {
if (kondisi) {
++lulus;
std::println("[ LULUS ] {}", deskripsi);
} else {
++gagal;
std::println("[ GAGAL ] {}", deskripsi);
}
}
int main() {
const std::vector<std::pair<std::string, RincianKerja>> karyawan = {
{"Budi", {.jamNormal = 160, .jamLembur = 10, .tarifPerJam = 25000,
.level = Level::Senior, .ikutAsuransi = true}},
{"Siti", {.jamNormal = 152, .jamLembur = 0, .tarifPerJam = 25000,
.level = Level::Menengah, .ikutAsuransi = false}},
{"Dewi", {.jamNormal = 80, .jamLembur = 12, .tarifPerJam = 20000,
.level = Level::Junior, .ikutAsuransi = true}},
};
std::println("== slip gaji ==");
for (const auto& [nama, rincian] : karyawan) {
cetakSlip(nama, rincian);
}
std::println("== rekap bulan ini ==");
int total = 0;
for (const auto& [nama, rincian] : karyawan) {
int bersih = gajiBersih(rincian);
std::println("{:<5}: Rp{}", nama, bersih);
total += bersih;
}
std::println("total: Rp{}", total);
std::println("== kunci perilaku ==");
cek(gajiBersih({.jamNormal = 160, .jamLembur = 10, .tarifPerJam = 25000,
.level = Level::Senior, .ikutAsuransi = true}) == 5670000,
"gaji bersih Budi = 5670000");
cek(gajiBersih({.jamNormal = 152, .jamLembur = 0, .tarifPerJam = 25000,
.level = Level::Menengah, .ikutAsuransi = false}) == 4560000,
"gaji bersih Siti = 4560000");
cek(gajiBersih({.jamNormal = 80, .jamLembur = 12, .tarifPerJam = 20000,
.level = Level::Junior, .ikutAsuransi = true}) == 2307000,
"gaji bersih Dewi = 2307000");
std::println("ringkasan: {} lulus, {} gagal", lulus, gagal);
return gagal == 0 ? 0 : 1;
}Output yang Dihasilkan:
== slip gaji ==
--- Budi ---
upah normal : Rp4000000
upah lembur : Rp500000
tunjangan : Rp1500000
gaji kotor : Rp6000000
potongan : Rp330000
gaji bersih : Rp5670000
--- Siti ---
upah normal : Rp3800000
upah lembur : Rp0
tunjangan : Rp1000000
gaji kotor : Rp4800000
potongan : Rp240000
gaji bersih : Rp4560000
--- Dewi ---
upah normal : Rp1600000
upah lembur : Rp360000
tunjangan : Rp500000
gaji kotor : Rp2460000
potongan : Rp153000
gaji bersih : Rp2307000
== rekap bulan ini ==
Budi : Rp5670000
Siti : Rp4560000
Dewi : Rp2307000
total: Rp12537000
== kunci perilaku ==
[ LULUS ] gaji bersih Budi = 5670000
[ LULUS ] gaji bersih Siti = 4560000
[ LULUS ] gaji bersih Dewi = 2307000
ringkasan: 3 lulus, 0 gagalBukti keberhasilan refactor ada di output: byte-per-byte identik dengan versi sebelum, dan ketiga cek tetap [ LULUS ] dengan exit code 0. Bedah keputusan strukturnya:
- Baris 10 (
enum class Level):level == 3yang licin menjadiLevel::Senioryang tak bisa keliru; nilai eksplisit 1-3 menjaga pemetaan data lama — perubahan struktur tanpa mengubah perilaku. - Baris 13-19 (
struct RincianKerja): lima parameter yang berubah bersama menjadi satu object; di call site (baris 81-89) setiap nilai menyebut namanya sendiri, dan tipe-tipe serupa tak lagi bisa tertukar urutan tanpa ditangkap compiler. - Baris 22-24 (
constexprbernama): aturan tunjangan, iuran, dan pajak naik ke permukaan sebagai konstanta bernama — persis langkah dua di daftar di atas. - Baris 27-34 (
upahLembur): extract function untuk aturan yang paling berisiko diubah; sebelumnya tertulis dua kali, kini satu rumah — perbaikan tarif lembur berikutnya cukup dilakukan sekali. - Baris 37-55 (function perhitungan):
tunjangan,gajiKotor,potongan,gajiBersihadalah function murni — masukan struct, keluaran angka, tanpa cetak; bentuk paling mudah diuji unit (recap chapter Separation of Concerns tentang lapisan logika). - Baris 57-65 (
cetakSlip): lapisan presentasi tidak lagi menghitung apa pun — ia menampilkan; perubahan format slip tak akan menyentuh satu angka aturan bisnis pun. - Baris 91 dan 97 (
structured bindingsdalam loop): data tiga karyawan menjadi satustd::vectoryang diloop dua kali; tiga baris call dengan argumen duplikat yang harus dijaga sinkron di versi sebelum kini mustahil tidak sinkron, karena datanya satu.
Perhatikan pula hasil samping yang sehat: versi sesudah lebih panjang beberapa baris, dan itu wajar — struktur yang jelas memang menulis lebih banyak baris deklarasi, tetapi masing-masing baris kini punya satu maksud yang bisa disebut namanya.
Penutup Section: Modularity dan Software Design #
Sebelas chapter section ini membentuk satu garis yang utuh — dari program yang dipilah-pilah sampai program yang siap diubah:
| Chapter | Bekal yang ditinggalkan |
|---|---|
| Memecah Program Menjadi Modul | Responsibility, kohesi, coupling sebagai kriteria pemisahan |
| Abstraction dan Encapsulation | Antarmuka vs implementasi, level abstraction, idiom PIMPL |
| Separation of Concerns | Parsing, logika, presentasi sebagai lapisan yang berdiri sendiri |
| Dependency Management di Level Desain | Arah dependensi, kopling longgar, inversion |
| SOLID Principles | SRP, OCP, LSP, ISP, DIP dalam kode C++ nyata |
| Composition over Inheritance | is-a vs has-a, decorator, strategy, tabel keputusan |
| Design Patterns | Strategy, Observer, Factory Method, Adapter — dan kritik Singleton |
| Dependency Injection | Constructor injection, test double, composition root |
| API Design | Nama eksplisit, parameter minim, return type jujur, noexcept, const |
| Rule of Zero | Kebijakan fungsi khusus: nol secara default, lima saat mengelola resource |
| Refactoring | Code smell, langkah kecil yang selalu hijau, bukti perilaku terjaga |
Kemampuan yang paling bernilai dari rangkaian ini bukan daftar istilahnya, melainkan satu kebiasaan: mengubah program dari bentuk yang bekerja menjadi bentuk yang bisa terus bekerja sambil tumbuh. Keputusan desain yang kita latih selalu dibaca dari satu sisi — benar atau salah secara struktur. Ada satu pertanyaan lagi yang belum kita minta pertanggungjawabannya: cepat atau lambat, mahal atau murah. Program modular yang rapi pun bisa menghabiskan waktu eksekusi sepuluh kali lebih lama daripada yang seharusnya, dan sebaliknya, pengutamaan yang asal dilakukan bisa merusak struktur yang sudah benar. Section berikutnya — Performa dan Optimasi — membahas sisi itu, dimulai dari fondasinya: memahami biaya komputasi sebelum memutuskan apa yang perlu dipercepat.
Referensi: Enumeration declaration (cppreference) , constexpr (cppreference) .