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:
OrderServicedi 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'" |
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;
}
}
calculateAmount(cartA, 'HEMAT10')sekarang bisa dicek sendiri:220500, tanpa pesanan tersimpan dan tanpa email. Empat keranjang tetapOK, denganPesanan tersimpan: 0danEmail terkirim: 0.- Permintaan CS cuma menyentuh
OrderMailer. Rumus harga gak dibuka sama sekali. placeOrder(cartD, 'HEMAT10', '[email protected]')mengembalikan{"id":1,"email":"[email protected]","total":204000}dan mengirim[email protected]: Pesanan #1, Total: Rp 204.000, sama persis dengan versi satu class. Ini refactor.
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:
EmailValidatordenganisValid(email):truekalau email mengandung@.UserRepositorydengansave(user): mengembalikan{ id: 1, ...user }.UserFormatterdenganformat(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
- Menghitung baris, bukan pihak. Method 15 baris bisa punya lima alasan berubah. Tanya "siapa saja yang bisa minta ini diubah?"
- Menganggap perangkai melanggar SRP. Fungsi yang cuma memanggil langkah-langkah berurutan punya satu alasan berubah: urutannya.
- Memecah sampai tiap baris jadi class. Kalau gak ada pihak berbeda yang minta perubahan, pecahan baru cuma menambah file.
- Test yang butuh banyak tiruan. Kalau mengecek satu hal butuh database dan email palsu, unit itu pegang terlalu banyak.
- Memformat data yang belum lengkap. Di latihan,
format(user)tanpaidmenghasilkan[undefined]. Format hasilsave.
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]).