Fungsi Kecil & Fokus — Clean Code

Fungsi yang baik melakukan satu hal, melakukannya dengan baik, dan hanya satu hal itu saja. Jika fungsi terlalu panjang atau melakukan terlalu banyak, pecah…

Fungsi yang baik melakukan satu hal. Ukurannya bukan jumlah baris, tapi apakah isinya bisa diceritakan dalam satu kalimat tanpa kata "dan". Bayangin resep nasi goreng. Di daftar menu cuma satu baris, tapi di dapur dipecah jadi langkah bernama: tumis bumbu, masukkan nasi, bumbui, sajikan. Kalau rasanya keasinan, kamu cukup memeriksa langkah "bumbui", gak perlu membaca resep dari awal.

🧪 Tebakan cepat: fungsi calculateTotal di bawah cuma 9 baris. Berapa hal yang dikerjakannya: satu, dua, atau empat? Tebak dulu, jawabannya ada di bagian "Satu kalimat tanpa dan".

Satu kalimat tanpa "dan"

Ini kasir Toko Kaos Raka setelah namanya dirapikan di lesson sebelumnya:

const VOUCHER_DISCOUNT = 0.1;
const FREE_SHIPPING_MIN = 200000;
const SHIPPING_FEE = 15000;

function calculateTotal(cart, voucherCode) {
  let total = 0;
  for (const item of cart) {
    total += item.price * item.qty;
  }
  if (voucherCode === 'HEMAT10') total -= total * VOUCHER_DISCOUNT;
  if (total < FREE_SHIPPING_MIN) total += SHIPPING_FEE;
  return 'Total: Rp ' + total.toLocaleString('id-ID');
}

Ceritakan isinya dalam satu kalimat: calculateTotal menjumlahkan harga barang, dan memotong voucher, dan menambah ongkir, dan memformat jadi teks rupiah. Tiga "dan", jadi empat hal. Itu jawaban tebakannya: cuma 9 baris, tapi empat pekerjaan. Panjang fungsi bukan ukurannya, yang diukur adalah berapa hal yang dikerjakannya sendiri.

Pecah per langkah

Tiap langkah dipindah ke fungsi sendiri dengan nama kata kerja:

function calculateSubtotal(cart) {
  let subtotal = 0;
  for (const item of cart) {
    subtotal += item.price * item.qty;
  }
  return subtotal;
}

function applyVoucher(amount, voucherCode) {
  if (voucherCode === 'HEMAT10') return amount - amount * VOUCHER_DISCOUNT;
  return amount;
}

function addShipping(amount) {
  if (amount < FREE_SHIPPING_MIN) return amount + SHIPPING_FEE;
  return amount;
}

function formatRupiah(amount) {
  return 'Rp ' + amount.toLocaleString('id-ID');
}

function calculateTotal(cart, voucherCode) {
  let total = calculateSubtotal(cart);
  total = applyVoucher(total, voucherCode);
  total = addShipping(total);
  return 'Total: ' + formatRupiah(total);
}

Pecah tanpa mengubah urutan

Memecah fungsi adalah refactor, jadi hasilnya harus tetap. Jalankan empat keranjang dari lesson pertama sebelum dan sesudah memecah:

Keranjang Voucher Hasil yang harus tetap
A: 2 kaos polos + 1 kaos sablon (245.000) HEMAT10 Total: Rp 220.500
A tanpa Total: Rp 245.000
C: 1 kaos polos (75.000) tanpa Total: Rp 90.000
D: 1 kaos polos + 1 kaos sablon + 1 topi (210.000) HEMAT10 Total: Rp 204.000

Kalau waktu memecah dua baris di calculateTotal tertukar, ongkir dicek sebelum potongan:

total = addShipping(total);               // tertukar
total = applyVoucher(total, voucherCode);
OK   Total: Rp 220.500
OK   Total: Rp 245.000
OK   Total: Rp 90.000
BEDA Total: Rp 189.000, harusnya Total: Rp 204.000

Tiga keranjang tetap lolos, cuma D yang menangkapnya. Kabar baiknya, setelah dipecah urutan langkah terbaca jelas sebagai dua baris yang bersebelahan.

Fungsi perangkai juga melakukan satu hal

calculateTotal yang cuma memanggil empat langkah tetap melakukan satu hal: menghitung total. Keempat langkahnya bagian dari tugas yang sama. Lain cerita dengan fungsi seperti ini:

function processOrder(order) {
  validateOrder(order);
  calculateTotal(order);
  sendConfirmationEmail(order);
  updateInventory(order);
  generateInvoice(order);
}

Walau sudah mendelegasikan, processOrder merangkai lima urusan dari bagian yang berbeda: pesanan, harga, email, stok gudang dan dokumen. Tiap kali salah satunya berubah syarat atau urutannya (misalnya email baru dikirim setelah pembayaran lunas), fungsi ini ikut diubah. Jadi ia masih melakukan terlalu banyak hal, dan lebih fokus kalau dipecah per urusan, misalnya langkah pesanan sendiri, lalu email, stok dan invoice ditangani setelah pesanan tersimpan.

Latihan: laporan nilai siswa

processStudents di latihan juga melakukan tiga hal sekaligus. Pecah jadi:

  1. filterPassingStudents(students): mengembalikan siswa dengan grade >= 70.
  2. calculateAverage(students): mengembalikan rata-rata grade. Daftar kosong mengembalikan 0.
  3. formatReport(passing, average): mengembalikan teks Lulus: X siswa, Rata-rata: Y, dengan X jumlah siswa di passing.

processStudents tinggal memanggil ketiganya berurutan. Dari lima siswa, yang lulus Budi 85, Citra 92 dan Eka 78. Rata-ratanya (85 + 92 + 78) / 3 = 85, jadi output-nya tetap Lulus: 3 siswa, Rata-rata: 85.

Kenapa daftar kosong harus dicek? 0 / 0 di JavaScript hasilnya NaN (Not a Number), bukan 0. Tulis if (students.length === 0) return 0; sebelum membagi.

⚠️ Jebakan yang sering ketemu

TL;DR: Ukur fungsi dari berapa hal yang dikerjakannya, bukan dari panjangnya: ceritakan dalam satu kalimat, dan tiap "dan" berarti satu hal lagi. calculateTotal 9 baris ternyata mengerjakan empat hal, jadi dipecah jadi calculateSubtotal, applyVoucher, addShipping dan formatRupiah, dan calculateTotal tinggal merangkai. Tiap fungsi kecil bisa dites sendiri. Memecah adalah refactor: urutannya harus tetap, dan keempat keranjang (keranjang D Total: Rp 204.000 menangkap urutan yang tertukar) harus memberi hasil yang sama. Fungsi perangkai boleh, selama langkahnya satu tugas; processOrder yang merangkai lima urusan berbeda masih melakukan terlalu banyak. Di latihan: lulus >= 70, rata-rata daftar kosong 0 (bukan NaN), laporan Lulus: 3 siswa, Rata-rata: 85.

Yang akan kamu pelajari