Anti-patterns & Code Smells — Design Patterns

Design pattern sering menyelamatkan kode. Tapi salah pakai pattern (atau pakai pattern padahal tidak perlu) menghasilkan anti-pattern. Kenali gejalanya…

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, dan SERVICE_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 tabel TARIF biasa? 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:

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():

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

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().

Yang akan kamu pelajari