Refactoring patterns adalah refactoring yang lebih besar dan punya nama: bukan cuma merapikan isi satu fungsi seperti di lesson Refactoring Techniques, tapi memindah tanggung jawab antar fungsi dan class. Aturannya tetap sama: struktur berubah, hasil gak berubah, dicek dengan keranjang test. Bayangin kasir Raka yang memegang buku catatan panjang: "kalau vouchernya jenis persen, hitung begini; kalau potongan rupiah, begitu; kalau gratis ongkir, lain lagi". Tiap jenis voucher baru, bukunya ditulis ulang. Jauh lebih gampang kalau tiap kartu voucher membawa aturannya sendiri di baliknya: kasir cukup bilang "kartu, hitung potonganmu".
🧪 Tebakan cepat: Raka minta jenis voucher keempat,
ONGKIR5K: ongkir cuma Rp 5.000. Berapa fungsi lama yang harus kamu ubah di versi switch, dan berapa di versi class? Tebak dulu, jawabannya ada di bagian "Jenis keempat: ONGKIR5K".
Tiga jenis voucher, dua tempat bertanya "jenis apa?"
Tabel VOUCHERS dari lesson sebelumnya cuma menyimpan tarif persen. ADR-0002 di lesson Documentation as Code sudah mencatat batasnya: potongan Rp 25.000 gak muat. Raka sekarang punya tiga jenis: HEMAT10 (10%), POTONG25 (potongan Rp 25.000) dan GRATISONGKIR. Cara pertama yang terpikir, type dan switch:
const VOUCHERS = {
HEMAT10: { type: 'percent', value: 0.1 },
POTONG25: { type: 'fixed', value: 25000 },
GRATISONGKIR: { type: 'shipping' },
};
function applyVoucher(amount, voucherCode) {
const voucher = VOUCHERS[voucherCode];
if (!voucher) return amount;
switch (voucher.type) {
case 'percent': return amount - amount * voucher.value;
case 'fixed': return Math.max(0, amount - voucher.value);
default: return amount;
}
}
function addShipping(amount, voucherCode) {
const voucher = VOUCHERS[voucherCode];
if (voucher && voucher.type === 'shipping') return amount;
if (amount < FREE_SHIPPING_MIN) return amount + SHIPPING_FEE;
return amount;
}
Empat keranjang lama ditambah dua cek baru: keranjang C (Rp 75.000) dengan POTONG25 jadi Total: Rp 65.000, dengan GRATISONGKIR jadi Total: Rp 75.000. Keenamnya OK. Tapi pengetahuan tentang jenis voucher tersebar di dua fungsi: switch di applyVoucher dan if di addShipping.
Replace Conditional with Polymorphism
Tiap jenis jadi class sendiri, dengan method yang sama: apply(amount) untuk potongan dan shipping(amount) untuk ongkir. Polymorphism artinya objek yang berbeda dipanggil dengan cara yang sama, dan tiap objek menjawab dengan aturannya sendiri:
class NoVoucher {
apply(amount) { return amount; }
shipping(amount) { return amount < FREE_SHIPPING_MIN ? SHIPPING_FEE : 0; }
}
class PercentVoucher extends NoVoucher {
constructor(rate) { super(); this.rate = rate; }
apply(amount) { return amount - amount * this.rate; }
}
class FixedVoucher extends NoVoucher {
constructor(cut) { super(); this.cut = cut; }
apply(amount) { return Math.max(0, amount - this.cut); }
}
class FreeShippingVoucher extends NoVoucher {
shipping() { return 0; }
}
const VOUCHERS = {
HEMAT10: new PercentVoucher(0.1),
POTONG25: new FixedVoucher(25000),
GRATISONGKIR: new FreeShippingVoucher(),
};
function calculateTotal(cart, voucherCode) {
const voucher = VOUCHERS[voucherCode] ?? new NoVoucher();
const discounted = voucher.apply(calculateSubtotal(cart));
return 'Total: ' + formatRupiah(discounted + voucher.shipping(discounted));
}
NoVoucheradalah perilaku tanpa voucher: gak ada potongan, ongkir biasa. Kode yang gak dikenal jatuh ke sini lewat?? new NoVoucher().- Tiap subclass cuma menimpa (override) method yang beda.
FreeShippingVouchergak menulis ulangapply. calculateTotalgak punya satu punswitchatautype. Ia cuma bilang "voucher, hitung".- Keenam cek tetap
OK. Ini refactor, bukan fitur.
Di dalamnya ada refactoring kedua, Move Function: cek "gratis ongkir?" pindah dari addShipping ke FreeShippingVoucher.shipping, tempat data yang ia pakai. Fungsi sebaiknya tinggal dekat data yang paling sering ia sentuh.
Jenis keempat: ONGKIR5K
Di versi switch, ONGKIR5K butuh satu baris di tabel dan satu fungsi lama diubah: addShipping dapat cabang baru. applyVoucher gak perlu diubah, tapi kamu harus membacanya dulu untuk tahu itu. Kalau addShipping terlewat, gak ada error:
BEDA Total: Rp 90.000, harusnya Total: Rp 80.000
Di versi class, nol fungsi lama diubah: satu class baru, CheapShippingVoucher, yang cuma menimpa shipping dengan amount < FREE_SHIPPING_MIN ? 5000 : 0, dan satu baris ONGKIR5K: new CheapShippingVoucher() di tabel.
Itu jawaban tebakannya: satu lawan nol. Ketujuh cek OK. Aturan ONGKIR5K tinggal di satu tempat, jadi gak ada cabang yang bisa terlewat. Ini juga yang dimaksud prinsip Open/Closed di lesson berikutnya. Contoh klasik bentuk yang sama: if (shape.type === 'circle') ... else if (shape.type === 'square') jadi class Circle dan Square yang masing-masing punya area().
Introduce Parameter Object
Label kiriman butuh nama, alamat dan nomor HP. Tiga string berurutan gampang tertukar, dan JavaScript gak protes:
function shippingLabel(name, address, phone) {
return 'Kepada: ' + name + ', ' + address + ' (' + phone + ')';
}
shippingLabel('Jl. Kopi 7, Bandung', 'Sari', '0812-1111-2222');
// Kepada: Jl. Kopi 7, Bandung, Sari (0812-1111-2222)
Introduce Parameter Object menggabungkan data yang selalu jalan bareng jadi satu objek:
function shippingLabel(customer) {
return 'Kepada: ' + customer.name + ', ' + customer.address + ' (' + customer.phone + ')';
}
const sari = { name: 'Sari', address: 'Jl. Kopi 7, Bandung', phone: '0812-1111-2222' };
shippingLabel(sari); // Kepada: Sari, Jl. Kopi 7, Bandung (0812-1111-2222)
Urutan properti gak penting lagi, dan menambah note untuk kurir cuma mengubah objek dan fungsi yang memakainya, bukan tiap tanda tangan fungsi yang mengopernya.
Latihan: refactoring yang dikerjakan editor
Beberapa refactoring cukup mekanis untuk dikerjakan alat: VS Code punya Rename Symbol (F2) dan Extract Function, dan linter menandai fungsi yang gak pernah dipakai. Latihan meniru tiga di antaranya pada string kode:
extractMethod(code, methodName)membungkus kode jadi`function ${methodName}() {\n ${code}\n}`. Hasilnya mengandungfunction calculateSum, jadiincludesmencetaktrue.renameVariable(code, oldName, newName)memakaicode.replace(new RegExp('\\b' + oldName + '\\b', 'g'), newName).\badalah batas kata, jadi cuma kata utuhdyang diganti; flaggmengganti semuanya.'let d = d + da;'jadi'let hari = hari + da;'. Tanpa\b, hasilnyalet hari = hari + haria;; tanpag, cumadpertama yang berganti.removeDeadCode(functions, usedFunctions)memakaifunctions.filter(fn => usedFunctions.includes(fn)): yang tersisa yang dipakai, dengan urutan daftar aslinya.(['a', 'b', 'c'], ['c', 'a'])jadi["a","c"].
Output latihan tiga baris: true, const currentDate = new Date();, dan ["fetchUser","formatDate"].
Kapan refactor: setelah hijau
Martin Fowler di buku Refactoring (edisi kedua, 2018) mendefinisikan refactoring sebagai perubahan struktur internal supaya kode lebih gampang dipahami dan lebih murah diubah, tanpa mengubah perilaku yang terlihat. Bukan menulis ulang dari nol, bukan menambah fitur, bukan mempercepat program.
Di TDD (Test-Driven Development, Kent Beck, 2002) urutannya Red, Green, Refactor: tulis test yang gagal (merah), bikin lulus dengan cara paling sederhana (hijau), baru rapikan. Refactor dilakukan setelah hijau, karena saat itu test membuktikan perilakunya benar dan menjaga tiap langkah. Switch tadi lulus enam cek dulu, baru dipecah jadi class.
⚠️ Jebakan yang sering ketemu
- Refactor sambil menambah jenis voucher. Pecah switch jadi class dulu sampai keenam cek
OK, commit, baru tambahONGKIR5K. - Mengubah semua switch jadi class. Switch yang muncul sekali dan jarang bertambah cabangnya lebih gampang dibaca daripada lima class. Polymorphism layak kalau pertanyaan "jenis apa?" muncul di beberapa tempat.
- Subclass yang menulis ulang semua method. Kalau
FreeShippingVoucherjuga menyalinapply, aturan potongan ada di dua tempat lagi. - Di latihan, rename tanpa
\batau tanpag./d/gmerusakdajadiharia;replace('d', ...)cuma mengganti yang pertama. - Di latihan, menyusun ulang urutan.
removeDeadCodemenyaringfunctions, bukanusedFunctions, supaya urutannya tetap["a","c"].
TL;DR: Refactoring patterns memindah tanggung jawab antar fungsi dan class tanpa mengubah hasil, dicek dengan keranjang test, dan dikerjakan setelah test hijau (Red, Green, Refactor). Replace Conditional with Polymorphism: tiga jenis voucher yang ditanya di switch applyVoucher dan if addShipping jadi class PercentVoucher, FixedVoucher, FreeShippingVoucher dengan apply dan shipping; keenam cek tetap OK, dan jenis keempat ONGKIR5K mengubah nol fungsi lama (versi switch: satu, dan kalau terlewat, Rp 90.000 bukan Rp 80.000). Move Function memindah fungsi ke data yang ia pakai. Introduce Parameter Object mengganti (name, address, phone) yang bisa tertukar dengan satu objek customer. Refactoring adalah perubahan struktur tanpa mengubah perilaku yang terlihat (Fowler). Di latihan: extractMethod membungkus kode jadi function nama(), renameVariable memakai replace dengan \b dan g, removeDeadCode memakai filter dengan urutan asli; output true, const currentDate = new Date();, ["fetchUser","formatDate"].