Code smell adalah tanda di permukaan kode yang biasanya menunjuk ke masalah lebih dalam: kode yang mahal diubah. Istilahnya dipopulerkan Kent Beck dan Martin Fowler di bab "Bad Smells in Code" buku Refactoring (1999). Bayangin bau aneh dari kulkas. Bau itu belum bukti ada makanan basi, tapi cukup jadi alasan untuk membuka dan mengecek. Smell juga begitu: bukan bug, tapi undangan untuk melihat lebih dekat, dan tiap smell punya nama dan obat refactoring-nya sendiri.
🧪 Tebakan cepat:
kasir-lama.jsdi bawah punya sebelas smell. Dari empat keranjang test (ditambah satu denganPOTONG25), berapa yangBEDA? Tebak dulu, jawabannya ada di bagian "Smell bukan bug".
kasir-lama.js
File ini ditemukan di folder lama Toko Kaos Raka, sebelum track ini dimulai. Masih dipakai, masih jalan:
const laporan = []; // pura-pura file laporan
const outbox = []; // pura-pura kotak keluar email
function prosesPesanan(nama, email, alamat, hp, k, v) {
// cek email
if (email.indexOf('@') < 0) return 'Email salah';
// hitung total
let t = 0;
for (let i = 0; i < k.length; i++) {
t = t + k[i].price * k[i].qty;
}
if (v == 'HEMAT10') {
t = t - t * 0.1;
} else if (v == 'POTONG25') {
t = t - 25000;
}
if (t < 200000) {
t = t + 15000;
}
// kirim email
outbox.push(email + ': Total Rp ' + t.toLocaleString('id-ID'));
// simpan laporan
laporan.push(nama + ', ' + alamat + ', ' + hp + ': Rp ' + t.toLocaleString('id-ID'));
return 'Total: Rp ' + t.toLocaleString('id-ID');
}
function labelKirim(nama, alamat, hp) {
return 'Kepada: ' + nama + ', ' + alamat + ' (' + hp + ')';
}
function hitungDiskonLama(t) {
return t * 0.15;
}
Smell bukan bug
Jalankan keranjang test-nya lewat prosesPesanan:
OK Total: Rp 220.500
OK Total: Rp 245.000
OK Total: Rp 90.000
OK Total: Rp 204.000
OK Total: Rp 65.000
Nol yang BEDA. Itu jawaban tebakannya. Hasilnya benar; yang bermasalah adalah biaya mengubahnya. Smell adalah indikasi, bukan bug, dan kadang bentuk itu tetap tepat untuk konteksnya: switch kecil di parser sederhana, atau fungsi panjang yang cuma urutan langkah lurus. Pertanyaan penentunya: apakah kode ini masih gampang diubah besok?
Katalog dari satu file
Sebelas smell di kasir-lama.js, masing-masing dengan perubahan yang jadi mahal dan obat dari lesson sebelumnya:
| Smell | Di kasir-lama.js | Perubahan yang jadi mahal | Obatnya |
|---|---|---|---|
| Long Function | prosesPesanan, 22 baris |
tiap perubahan harus membaca semuanya | Extract Function |
| Long Parameter List | 6 parameter | urutan gampang tertukar | Introduce Parameter Object |
| Data Clumps | nama, alamat, hp selalu bareng, di dua fungsi |
menambah kode pos mengubah tiap tanda tangan | Introduce Parameter Object |
| Magic Number | 0.1, 25000, 200000, 15000 |
ganti batas ongkir: angka yang mana? | Replace Magic Number with Symbolic Constant |
| Duplicated Code | toLocaleString('id-ID') 3 kali |
ganti format rupiah: 3 baris | Extract Function (formatRupiah) |
| Repeated Switches | if (v == 'HEMAT10') ... else if |
voucher baru mengedit fungsi ini | Lookup table atau Polymorphism |
| Primitive Obsession | email cuma string, dicek indexOf('@') |
aturan email berubah: cari semua pengeceknya | Replace Primitive with Object |
| Divergent Change | prosesPesanan berubah karena 5 alasan |
sapaan email membuka rumus harga | Pecah per alasan (SRP) |
| Mysterious Name | k, v, t |
tiap pembaca menebak | Rename |
| Comments | // cek email, // hitung total |
komentar menambal nama yang kurang | Extract Function bernama |
| Dead Code | hitungDiskonLama, gak pernah dipanggil |
dibaca dan dirawat tanpa guna | Remove Dead Code |
Satu efek samping juga ketahuan saat test: lima cek harga di atas mengirim 5 email dan menulis 5 baris laporan, persis gejala yang dibahas di lesson Single Responsibility.
Pasangan yang sering tertukar
- Shotgun Surgery: satu perubahan kecil memaksa kamu mengedit banyak tempat, seperti tembakan shotgun yang pelurunya menyebar. Format rupiah di sini 3 baris dalam satu fungsi; di toko yang lebih besar, format laporan yang ditulis ulang di 6 file berbeda berarti 6 file disentuh untuk satu perubahan. Obatnya Move Function: kumpulkan ke satu tempat.
- Divergent Change: kebalikannya. Satu fungsi atau class berubah karena banyak alasan berbeda.
prosesPesanandiubah keuangan, gudang, CS dan tim IT. Obatnya pecah per alasan. - Data Clumps vs Primitive Obsession: Data Clumps adalah beberapa data yang selalu jalan bareng (
nama, alamat, hp). Primitive Obsession adalah konsep domain yang disimpan sebagai tipe dasar (stringuntuk email,numberuntuk uang), jadi validasi dan formatnya tersebar. - Feature Envy: fungsi yang lebih banyak memakai data objek lain daripada datanya sendiri. Obatnya Move Function ke objek itu.
Primitive Obsession dan Value Object
Email sebagai string berarti tiap tempat yang menerima email harus ingat mengeceknya. Replace Primitive with Object membuat value object: objek kecil yang membawa nilai dan aturannya sendiri.
class Email {
constructor(text) {
if (!text.includes('@')) throw new Error('Email tidak valid: ' + text);
this.text = text;
}
}
new Email('[email protected]'); // lolos
new Email('sari-mail.com'); // Error: Email tidak valid: sari-mail.com
Aturan email sekarang di satu tempat, dan fungsi yang menerima Email tahu isinya sudah dicek. Uang bisa diperlakukan sama: Rupiah dengan method format().
Mesin menemukan sebagian
Tiga smell bisa dihitung: jumlah parameter, angka di kode, jumlah baris. Detektor dari latihan, dijalankan ke ketiga fungsi kasir-lama.js:
prosesPesanan ["long-parameter-list","magic-number","long-function"]
labelKirim []
hitungDiskonLama ["magic-number"]
Sisanya butuh manusia. labelKirim bersih menurut mesin, padahal nama, alamat, hp di situ adalah Data Clumps. Shotgun Surgery, Divergent Change dan Feature Envy cuma kelihatan kalau kamu bertanya "apa yang berubah bareng?". Bahkan 0.1 lolos, karena pola \b\d+\b memecahnya jadi 0 dan 1, dua angka yang sengaja diabaikan. Alat seperti ESLint menangkap yang bisa dihitung; review menangkap sisanya.
Latihan: detectSmells
detectSmells(fnString) menerima kode sebuah fungsi sebagai string dan mengembalikan array nama smell, dengan urutan ini:
"long-parameter-list": lebih dari 3 parameter. Ambil isi kurung pertama setelahfunctiondengan/function\s*\w*\s*\(([^)]*)\)/, pecah dengansplit(','), buang yang kosong, hitung."magic-number": ada angka selain0dan1.fnString.match(/\b\d+\b/g) || [], lalufilteryang bukan'0'dan'1'."long-function": lebih dari 20 baris,fnString.split('\n').length > 20.
Tiap yang ketemu di-push ke smells. Output-nya:
["long-parameter-list","magic-number"]
[]
["magic-number","long-function"]
fn1 punya 4 parameter dan 42; fn2 bersih; fn3 punya 28 baris dan 99. Fungsi lain dengan ciri sama harus dikenali juga: function f(a,b,c,d) { return 7; } dapat dua smell, function g(a) { return a + 1; } gak dapat apa-apa.
⚠️ Jebakan yang sering ketemu
- Menganggap smell sebagai bug. Kelima cek
OK. Smell menunjuk ke biaya perubahan, bukan ke hasil yang salah. - Memperbaiki semua smell sekarang juga. Rapikan yang ada di jalur perubahan berikutnya;
hitungDiskonLamacukup dihapus,switchkecil yang gak pernah bertambah boleh tetap. - Menukar Shotgun Surgery dengan Divergent Change. Satu perubahan, banyak tempat: shotgun. Satu tempat, banyak alasan: divergent.
- Percaya penuh pada detektor.
labelKirimlolos dan0.1lolos. Mesin menghitung; manusia bertanya "apa yang berubah bareng?". - Di latihan, urutan atau angka 0 dan 1. Urutannya parameter, angka, panjang;
0dan1bukan magic number, jadifunction g(a) { return a + 1; }harus[].
TL;DR: Code smell adalah tanda kode yang mahal diubah, bukan bug (Kent Beck dan Martin Fowler, Refactoring, 1999): kasir-lama.js dengan sebelas smell tetap lolos kelima cek. Katalognya: Long Function dan Comments (Extract Function), Long Parameter List dan Data Clumps (Introduce Parameter Object), Magic Number (konstanta bernama), Duplicated Code (formatRupiah), Repeated Switches (tabel atau Polymorphism), Primitive Obsession (value object seperti Email), Divergent Change (pecah per alasan), Mysterious Name (Rename), Dead Code (hapus). Shotgun Surgery: satu perubahan menyentuh banyak tempat, misalnya format laporan di 6 file; Divergent Change: satu tempat berubah karena banyak alasan. Mesin cuma menemukan yang bisa dihitung. Di latihan: detectSmells mengembalikan long-parameter-list (> 3 parameter), magic-number (angka selain 0 dan 1) dan long-function (> 20 baris) dalam urutan itu, dengan output ["long-parameter-list","magic-number"], [], ["magic-number","long-function"].