Anti-pattern adalah solusi yang kelihatan masuk akal dan sering dipakai, tapi biayanya lebih besar dari manfaatnya. Code smell adalah tanda di kode yang patut dicurigai: belum tentu salah, tapi layak dicek. Bayangin bau gosong dari dapur. Baunya belum tentu kebakaran, mungkin cuma roti yang kelamaan dipanggang, tapi kamu pasti mengecek. Code smell itu baunya; anti-pattern itu resep yang kelihatannya benar tapi selalu bikin gosong. Lesson terakhir track ini membongkar repo lama Kurir Kancil: enam pattern dari track ini, dipakai di tempat yang salah.
🧪 Tebakan cepat: repo lama Kurir Kancil menghitung ongkir lewat
BasePricingStrategy,PricingStrategyFactory, danSERVICE_TYPES, "biar siap untuk masa depan". Layanan baru sameday Rp 25.000 per kg ditambahkan. Berapa tempat di kode yang harus diubah? Dan di versi tabelTARIFbiasa? Tebak dulu, jawabannya ada di bagian "Speculative generality".
Mengukur anti-pattern: biaya satu perubahan
Andrew Koenig (1995) memakai istilah anti-pattern untuk sesuatu yang kelihatan seperti solusi tapi bukan. Buku AntiPatterns (Brown dkk., 1998) mengumpulkan yang paling sering muncul, misalnya The Blob (god object) dan Golden Hammer. Istilah code smell datang dari Kent Beck, dan katalognya ada di buku Refactoring karya Martin Fowler (1999).
Cara menilainya: ambil satu perubahan yang realistis, kerjakan, lalu hitung tempat yang diubah, file yang disentuh, dan test yang gagal. Ini enam perubahan di repo lama dan di versi yang sudah dirapikan, dengan angka yang sama seperti di Lab:
| anti-pattern | perubahan | repo lama | dirapikan |
|---|---|---|---|
God facade (kurirKancil.js) |
SMS pindah ke WhatsApp | 3 tempat | 1 tempat |
Singleton global (resiCounter.js) |
tambah test kedua | 0 baris kode, 1 test gagal | 0 gagal |
Abstract factory 1 mitra (partnerKit.js) |
tambah asuransi Rp 5.000 | 3 tempat di 3 file | 2 tempat di 2 file |
Mediator serba tahu (dispatch.js) |
kurir disimpan di Map per id |
1 tempat, dispatch rusak | 1 tempat, 0 gagal |
Speculative generality (pricing.js) |
tambah sameday | 3 tempat | 1 tempat |
| Golden hammer (broker untuk ongkir) | tambah asuransi Rp 5.000 | total salah tanpa error | 1 tempat, 0 gagal |
Kode yang lebih pendek belum tentu lebih murah diubah: versi rapi god facade 24 baris, versi lamanya 18, tapi perubahannya 1 tempat lawan 3.
God facade
kurirKancil.js berisi kirim, refund, gajiKurir, dan laporanBulanan: urusan empat tim dalam satu object. Itu god object (Brown dkk. menyebutnya The Blob): satu class atau object yang tahu terlalu banyak dan mengerjakan terlalu banyak. Ia melanggar Single Responsibility: satu modul, satu alasan untuk berubah. Kabar ke pelanggan dan kurir dikirim langsung dari tiga method:
smsGateway.send({ to: hp, text: resi + " dijemput" }) // di kirim
smsGateway.send({ to: hp, text: resi + " direfund" }) // di refund
smsGateway.send({ to: hpKurir, text: "gaji Rp " + paket * 2500 }) // di gajiKurir
Pindah ke WhatsApp mengubah 3 tempat di file yang juga dipegang tim refund, gaji, dan laporan; lupa satu, dan kurir masih dapat SMS. Obatnya: pecah per tanggung jawab (Pengiriman, Penggajian, Laporan), dan biarkan satu modul yang kenal vendor. Perpindahannya jadi 1 baris:
const notifier = {
kirim: (hp, pesan) => waApi.sendMessage(hp.replace(/^0/, "62"), pesan),
}
Singleton sebagai state global
function buatResi() {
return ResiCounter.getInstance().next()
}
// lulus: pesan resi pertama
// gagal: resi pertama hari ini KCL-0001, dapat KCL-0002
Menambah test kedua mengubah 0 baris kode, tapi test-nya gagal: singleton membawa hitungan dari test pertama. Obatnya dari lesson Dependency Injection: buatResi(counter), main.js membuat satu counter untuk aplikasi, dan tiap test membuat createResiCounter() sendiri.
Abstract factory untuk satu keluarga
const PRODUK = ["createLabel", "createTracking"]
const KITS = { merpati: merpatiKit } // mitranya cuma satu
const kit = createPartnerKit("merpati")
Menambah asuransi berarti 3 tempat di 3 file: daftar PRODUK, merpatiKit, dan checkout.js. Lupa PRODUK, dan kit.createInsurance is not a function. Dengan function biasa (labelMerpati, lacakMerpati) kodenya 9 baris, bukan 19, dan asuransi cukup function asuransiMerpati(p) plus satu baris di checkout. Abstract factory baru terbayar saat mitra kedua datang.
Mediator yang tahu segalanya
const dispatch = {
pilihPaket(paket) {
const cocok = kurirList.items.filter((k) => k.zona === paket.zona) // mengintip isi kurirList
cocok.sort((a, b) => a.beban - b.beban)
kurirList.sorot = cocok[0].id
peta.titik = paket.zona
},
}
Tim daftar kurir mengganti items jadi byId: new Map(...) di file mereka sendiri, dan dispatch berhenti: TypeError: Cannot read properties of undefined (reading 'filter'). Mediator yang tahu isi semua komponen ikut berubah tiap ada tim yang berubah, dan lama-lama jadi god object. Obatnya: dispatch cuma memanggil method milik komponen, kurirList.cariUntuk(paket.zona), kurirList.sorotKurir(kurir.id), peta.tampilkan(paket.zona), dan isi tiap komponen jadi urusan komponen itu.
Speculative generality
pricing.js lama punya SERVICE_TYPES, class BasePricingStrategy dengan options = { currency: "IDR", locale: "id-ID", plugins: [] } yang belum pernah dipakai siapa pun, satu class per layanan, dan PricingStrategyFactory dengan registry: 32 baris untuk dua tarif. Sameday butuh 3 tempat: entri SAMEDAY di SERVICE_TYPES, class SamedayPricing, dan satu baris di registry, sampai kodenya jadi 38 baris. Versi tabelnya 4 baris dan butuh 1 tempat:
const TARIF = { reguler: 10000, kilat: 18000, sameday: 25000 } // per kg
function hitungOngkir(service, kg) {
return kg * TARIF[service]
}
Itu jawaban tebakannya: 3 lawan 1. Fowler menyebut ini speculative generality: kait, parameter, dan class dasar untuk kebutuhan yang cuma diramalkan. Obatnya prinsip dari lesson Kapan Pakai Pattern Apa, YAGNI (You Aren't Gonna Need It): tulis yang paling sederhana, lalu refactor ke pattern saat rasa sakitnya benar-benar muncul. Feedback yang tepat untuk PR berisi factory dari factory dari builder demi object 2 field juga YAGNI.
Golden hammer
Abraham Maslow (1966) menulis bahwa kalau satu-satunya alat yang kamu punya adalah palu, kamu tergoda memperlakukan semuanya seperti paku. Tim yang baru jatuh cinta pada broker dari lesson Pub/Sub menghitung ongkir lewat event:
broker.subscribe("hitung-ongkir", (order) => {
order.ongkir = order.kg * TARIF[order.service]
})
function checkout(order) {
broker.publish("hitung-ongkir", order)
return order.ongkir
}
Asuransi ditambahkan sebagai subscriber baru di asuransi.js, yang dimuat sebelum ongkir.js karena urut abjad. Ia jalan duluan, lalu ditimpa: total tetap Rp 36000, bukan Rp 41000, tanpa satu error pun. Pub/Sub cocok untuk banyak pihak yang saling gak kenal; untuk "hitung lalu kembalikan hasilnya", panggil function: return hitungOngkir(order) + 5000.
Code smell yang bisa dicari mesin
Beberapa smell bisa dideteksi otomatis dari teks kodenya:
- Long function: function yang terlalu panjang untuk dibaca sekali jalan. Latihan memakai batas lebih dari 20 baris.
- Magic number dan magic string: angka atau teks tanpa nama. Di
if (user.type === 3 && order.status === "A") { discount = price * 0.15 }, pembaca harus menebak arti3,"A", dan0.15. Obatnya konstanta bernama:const USER_TYPE_ADMIN = 3,const STATUS_AKTIF = "A",const DISKON_VIP = 0.15.paket * 2500di god facade jadipaket * UPAH_PER_PAKETdi versi rapinya. - Deep nesting:
ifdi dalamifdi dalamfor, empat tingkat kurung kurawal atau lebih. Obatnya guard clause (if (!order) return) dan function kecil.
Detektor cuma menemukan baunya; manusia yang menilai. Regex magic number juga menandai TARIF = { kilat: 18000 }, padahal angka itu sudah punya nama.
Latihan: detectSmells
Latihan nanti meminta detectSmells(source) yang menerima kode sebagai string dan mengembalikan array nama smell, diurutkan dengan sort():
"long-function"kalausource.split("\n").lengthlebih dari 20;"magic-number"kalau/\b(?!0\b|1\b)\d+\b/cocok (ada angka selain 0 dan 1);"deep-nesting"kalau kedalaman{paling tinggi 4 atau lebih: tambah 1 tiap{, kurangi 1 tiap}, catat maksimumnya.
Starter mencetak deep-nesting,long-function,magic-number untuk function 24 baris yang penuh masalah, baris kosong untuk add(a, b) yang bersih (array kosong di-join jadi teks kosong), magic-number untuk const x = 42, dan deep-nesting untuk tiga if bersarang di dalam function.
⚠️ Jebakan yang sering ketemu
- Membuang pattern yang memang dibutuhkan. Satu koneksi database tetap cocok dibuat sekali; abstract factory tetap tepat saat mitra kedua datang. Nilai dari biaya perubahannya, bukan dari nama pattern-nya.
- Merapikan semuanya sekaligus. Refactor dalam langkah kecil, dan jalankan test sesudah tiap langkah.
- Di latihan: lupa
sort(). Urutan temuanlong-function,magic-number,deep-nestinggak sama dengandeep-nesting,long-function,magic-numberyang dinilai. - Di latihan: salah batas. Lebih dari 20 baris (
> 20), dan kedalaman 4 atau lebih (>= 4). Kode pendekconst x = 42;cuma boleh mendapatmagic-number.
TL;DR: Anti-pattern adalah solusi yang kelihatan masuk akal tapi lebih mahal dari manfaatnya (Koenig 1995; Brown dkk. 1998); code smell adalah tanda yang layak dicek (Beck dan Fowler, Refactoring, 1999). Ukur dengan satu perubahan nyata: tempat yang diubah, file yang disentuh, test yang gagal. God facade (god object, melanggar Single Responsibility): SMS ke WhatsApp 3 tempat, dipecah jadi 1. Singleton global: test kedua dapat KCL-0002, obatnya DI. Abstract factory untuk satu mitra: 19 baris lawan 9. Mediator yang mengintip isi komponen rusak saat komponennya berubah. Speculative generality: sameday 3 tempat lawan 1 baris di TARIF, obatnya YAGNI. Golden hammer: ongkir lewat event membuat total salah tanpa error, panggil function saja. Smell yang dicari mesin: long function, magic number (dan magic string), deep nesting; di latihan, detectSmells mengembalikan array yang di-sort().