Single Responsibility — Clean Code

Single Responsibility Principle (SRP): Setiap fungsi, class, atau module harus punya satu alasan untuk berubah. Jika sebuah fungsi bisa berubah karena 2…

Single Responsibility Principle (SRP) artinya satu unit kode, entah fungsi, class atau file, cuma punya satu alasan untuk berubah. Cara paling gampang membayangkan "alasan berubah": siapa yang bakal datang minta kode itu diubah. Bayangin satu karyawan Toko Kaos Raka yang mengurus semuanya: cek pesanan, hitung harga, catat di buku, dan kirim pesan ke pembeli. Bagian keuangan mau ganti promo, gudang mau ganti ongkir, CS mau ganti kata-kata pesannya, dan semuanya mendatangi orang yang sama. Tiap permintaan bisa mengganggu kerjaan lain yang dipegang orang itu.

🧪 Tebakan cepat: OrderService di bawah mengurus validasi, harga, penyimpanan dan email dalam satu method. Kamu cuma mau mengecek harga empat keranjang. Berapa email yang ikut terkirim? Tebak dulu, jawabannya ada di bagian "Mengecek harga ikut mengirim email".

Satu class, empat pekerjaan

Toko Kaos Raka sekarang menyimpan pesanan dan mengirim email konfirmasi. Supaya bisa dijalankan di mana saja, "database" dan "kotak keluar email" di sini cuma array:

// VOUCHER_DISCOUNT, FREE_SHIPPING_MIN, SHIPPING_FEE: konstanta dari lesson sebelumnya
const savedOrders = []; // pura-pura database
const outbox = [];      // pura-pura kotak keluar email

class OrderService {
  placeOrder(cart, voucherCode, email) {
    if (cart.length === 0) throw new Error('Keranjang kosong');
    if (!email.includes('@')) throw new Error('Email tidak valid');

    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;

    const order = { id: savedOrders.length + 1, email, total };
    savedOrders.push(order);

    outbox.push(email + ': Pesanan #' + order.id + ', Total: Rp ' + total.toLocaleString('id-ID'));
    return order;
  }
}

Empat blok yang dipisah baris kosong: memeriksa pesanan, menghitung harga, menyimpan, mengirim email. Ceritakan dengan satu kalimat dan kamu butuh tiga "dan".

Alasan berubah = siapa yang minta

Hitung pihak yang bisa datang minta placeOrder diubah:

Yang minta Permintaannya Bagian yang berubah
Raka, pemilik toko "Maksimal 10 barang per pesanan" validasi
Keuangan "HEMAT10 jadi 15%" voucher
Gudang "Ongkir jadi Rp 20.000" ongkir
Tim IT "Pindah dari array ke database beneran" penyimpanan
CS "Email dibuka dengan 'Halo kak'" email

Empat pekerjaan, tapi lima pihak, karena harga punya dua pemilik. Semuanya di satu method. Permintaan CS soal sapaan email memaksa kamu membuka method yang sama dengan rumus harga, dan tiap kali selesai mengubah, semua pekerjaannya harus dicek ulang.

Cara menghitungnya sama untuk kode lain. saveUserAndSendEmail() sudah ketahuan dari namanya: dua pekerjaan, jadi dua alasan berubah. UserService yang menangani autentikasi, validasi input, query database dan format response punya empat alasan berubah, karena tiap tanggung jawab bisa berubah sendiri-sendiri. Mengubah format response gak seharusnya bisa merusak login.

Mengecek harga ikut mengirim email

Cara satu-satunya mengecek harga di class ini adalah memanggil placeOrder. Jalankan empat keranjang dari lesson pertama:

OK   Total: Rp 220.500
OK   Total: Rp 245.000
OK   Total: Rp 90.000
OK   Total: Rp 204.000
Pesanan tersimpan: 4
Email terkirim: 4

Harganya benar, tapi buat mengecek empat harga, empat pesanan palsu masuk "database" dan empat email terkirim. Itu jawaban tebakannya. Di toko beneran, itu empat email nyasar dan empat baris sampah di laporan keuangan. Makanya test untuk class yang memegang banyak pekerjaan biasanya butuh banyak tiruan (mock): satu untuk database, satu untuk email, dan seterusnya.

Pecah per yang minta

Tiap pihak dapat unitnya sendiri. Harga sudah dipecah di lesson Fungsi Kecil & Fokus: applyVoucher milik keuangan, addShipping milik gudang. Tinggal tiga class baru:

class OrderValidator {          // aturan pesanan: Raka
  validate(cart, email) {
    if (cart.length === 0) throw new Error('Keranjang kosong');
    if (!email.includes('@')) throw new Error('Email tidak valid');
  }
}

class OrderRepository {         // penyimpanan: tim IT
  save(order) {
    const saved = { id: savedOrders.length + 1, ...order };
    savedOrders.push(saved);
    return saved;
  }
}

class OrderMailer {             // kata-kata email: CS
  sendConfirmation(order) {
    outbox.push(order.email + ': Pesanan #' + order.id + ', Total: ' + formatRupiah(order.total));
  }
}

function calculateAmount(cart, voucherCode) {   // harga: keuangan dan gudang
  const discounted = applyVoucher(calculateSubtotal(cart), voucherCode);
  return addShipping(discounted);
}

class OrderService {
  placeOrder(cart, voucherCode, email) {
    new OrderValidator().validate(cart, email);
    const total = calculateAmount(cart, voucherCode);
    const order = new OrderRepository().save({ email, total });
    new OrderMailer().sendConfirmation(order);
    return order;
  }
}

Perangkai bukan pelanggaran

placeOrder yang baru masih memanggil empat hal. Apa itu melanggar SRP? Gak. Ia cuma merangkai urutan langkah, dan alasan ia berubah cuma satu: urutan proses checkout berubah, misalnya email baru dikirim setelah pembayaran lunas. Sama seperti calculateTotal di lesson Fungsi Kecil.

Kebalikannya juga salah: memecah sampai tiap baris jadi class. add(a, b) yang dipecah jadi Validator, Calculator dan Formatter cuma menambah file tanpa memisahkan siapa pun, karena gak ada pihak berbeda yang minta perubahan.

Latihan: UserManager

UserManager di latihan mengerjakan tiga hal. Pecah jadi tiga class:

  1. EmailValidator dengan isValid(email): true kalau email mengandung @.
  2. UserRepository dengan save(user): mengembalikan { id: 1, ...user }.
  3. UserFormatter dengan format(user): mengembalikan '[' + user.id + '] ' + user.name + ' (' + user.email + ')'.

Untuk { name: 'Budi', email: '[email protected]' }, output-nya:

true
{"id":1,"name":"Budi","email":"[email protected]"}
[1] Budi ([email protected])

Format user yang sudah disimpan (saved), karena cuma dia yang punya id. Kalau yang diformat user aslinya, baris ketiga jadi [undefined] Budi ([email protected]).

⚠️ Jebakan yang sering ketemu

TL;DR: SRP artinya satu unit kode punya satu alasan berubah, dan alasan berubah paling gampang dihitung dari siapa yang minta perubahan. OrderService.placeOrder yang memeriksa, menghitung harga, menyimpan dan mengirim email punya lima pihak (Raka, keuangan, gudang, tim IT, CS), dan mengecek harga empat keranjang lewat method itu menyimpan 4 pesanan dan mengirim 4 email. Dipecah jadi OrderValidator, calculateAmount, OrderRepository dan OrderMailer, harga bisa dicek sendiri tanpa efek samping, dan placeOrder tinggal merangkai dengan hasil yang sama. saveUserAndSendEmail() melanggar SRP karena mengerjakan dua hal; UserService dengan autentikasi, validasi, query dan format punya 4 alasan berubah. Di latihan: EmailValidator, UserRepository, UserFormatter, dan format user hasil save supaya keluar [1] Budi ([email protected]).

Yang akan kamu pelajari