Setiap aplikasi menangani tiga jenis pekerjaan sekaligus: menyimpan data, menghitung sesuatu dari data itu, dan menampilkan hasilnya kepada pengguna. Ketiganya mudah ditulis menyatu dalam satu function — sampai aplikasi berubah. Pada toko daring Budi, satu function cetakLaporanHarian() membaca data penjualan, menghitung total, memilih produk terlaris, dan mencetak laporan. Function tersebut bekerja dengan benar, tetapi setiap perubahan kecil — format angka, sumber data, aturan “terlaris” — memaksa kita menyentuh bagian yang sama lagi dan lagi.
Separation of concerns (pemisahan keberatan) adalah kebiasaan menempatkan jenis pekerjaan yang berbeda ke tempat yang berbeda, sehingga satu perubahan hanya menjangkau satu tempat. Chapter ini membedah versi campur dari laporan penjualan Budi, lalu memisahkannya menjadi tiga lapisan dengan keluaran yang identik persis. Dua berkas contoh di chapter ini bisa dijalankan di terminal dan menghasilkan laporan yang sama.
Tiga Keberatan yang Sering Tercampur #
Kata concern (keberatan) berarti satu jenis tanggung jawab: sesuatu yang berubah karena alasan yang berbeda dari keberatan lain. Pada aplikasi kecil, tiga keberatan paling sering tertumpuk di satu tempat:
| Keberatan | Tugasnya | Contoh di laporan Budi |
|---|---|---|
| Data | Menyimpan dan menyediakan nilai | Array penjualan hari ini |
| Logika | Menghitung dan mengambil keputusan | Total pendapatan, produk terlaris |
| Tampilan | Memformat dan menampilkan keluaran | Format Rupiah, urutan baris laporan |
Ketiganya punya alasan berubah yang berbeda. Data berubah karena stok dan transaksi bertambah; logika berubah karena kebijakan bisnis berubah; tampilan berubah karena selera pembaca laporan berubah. Bila ketiganya menyatu, satu perubahan selalu menyentuh kode yang tidak berhubungan dengannya — di situlah kesalahan baru biasanya masuk.
Pola yang sama terlihat di aplikasi browser. Handler tombol yang menghitung diskon sekaligus menulis innerHTML adalah versi campur yang identik: data formulir, aturan diskon, dan elemen DOM berjimpit dalam satu function. Segera setelah perhitungan yang sama dibutuhkan di halaman lain, satu-satunya jalan adalah menyalinnya — dan salinan itulah yang berbeda isi enam bulan kemudian. Pemisahan keberatan bukan soal selera gaya, melainkan soal mempersiapkan waktu kita berikutnya.
Versi Campur: Semuanya dalam Satu Function #
Berkas berikut adalah kondisi awal yang sering kita jumpai di proyek kecil: data tertanam, penghitungan berjalan, dan pencetakan terjadi dalam satu function yang sama:
// laporan-campur.js — data, logika, dan tampilan bercampur dalam satu function.
// Jalankan di terminal: node laporan-campur.js
function cetakLaporanHarian() {
// Data tertanam langsung di dalam function.
const penjualan = [
{ produk: "Pulpen Gel", harga: 8000, jumlah: 12 },
{ produk: "Buku Tulis", harga: 5500, jumlah: 20 },
{ produk: "Penggaris", harga: 4000, jumlah: 5 },
];
// Penghitungan menyatu dengan pencetakan.
let total = 0;
let terlaris = penjualan[0];
for (const item of penjualan) {
total += item.harga * item.jumlah;
if (item.jumlah > terlaris.jumlah) {
terlaris = item;
}
}
// Pemformatan tampilan juga menyatu di sini.
const formatRp = new Intl.NumberFormat("id-ID");
console.log("=== Laporan Penjualan Harian ===");
for (const item of penjualan) {
console.log(`${item.produk}: ${item.jumlah} pcs x Rp ${formatRp.format(item.harga)}`);
}
console.log(`Terlaris: ${terlaris.produk}`);
console.log(`Total pendapatan: Rp ${formatRp.format(total)}`);
}
cetakLaporanHarian();Output yang Dihasilkan:
=== Laporan Penjualan Harian ===
Pulpen Gel: 12 pcs x Rp 8.000
Buku Tulis: 20 pcs x Rp 5.500
Penggaris: 5 pcs x Rp 4.000
Terlaris: Buku Tulis
Total pendapatan: Rp 226.000Mari kita telaah baris-baris pentingnya:
- Baris 4 (
function cetakLaporanHarian() {): satu function memegang tiga keberatan sekaligus. Namanya pun menyebutkan “cetak”, padahal di dalamnya ada penghitungan — tanda pertama keberatan yang tercampur. - Baris 6–10 (
const penjualan = [...]): data hidup di dalam badan function. Nilainya tidak bisa diubah dari luar tanpa menyunting kode, dan function lain tidak bisa memakainya ulang. - Baris 13–20 (penghitungan
totaldanterlaris): aturan bisnis (cara menghitung total, cara memilih terlaris) terjalin dengan struktur function. Tidak ada cara menguji aturan ini tanpa ikut menjalankan pencetakan. - Baris 23–29 (
console.log(...)): pemformatan Rupiah dan susunan baris menyatu dengan penghitungan. Mengganti keluaran menjadi CSV berarti menulis ulang seluruh function.
Memisahkan Menjadi Tiga Lapis #
Berkas kedua berisi pekerjaan yang sama, tetapi setiap keberatan mendapat tempatnya sendiri:
// laporan-terpisah.js — data, logika, dan tampilan masing-masing terpisah.
// Jalankan di terminal: node laporan-terpisah.js
// ===== Lapisan DATA: hanya menyimpan nilai, tidak menghitung dan tidak mencetak =====
const penjualan = [
{ produk: "Pulpen Gel", harga: 8000, jumlah: 12 },
{ produk: "Buku Tulis", harga: 5500, jumlah: 20 },
{ produk: "Penggaris", harga: 4000, jumlah: 5 },
];
// ===== Lapisan LOGIKA: pure function, menerima data dan mengembalikan hasil =====
function totalPendapatan(daftar) {
let total = 0;
for (const item of daftar) {
total += item.harga * item.jumlah;
}
return total;
}
function produkTerlaris(daftar) {
let terlaris = daftar[0];
for (const item of daftar) {
if (item.jumlah > terlaris.jumlah) {
terlaris = item;
}
}
return terlaris.produk;
}
// ===== Lapisan TAMPILAN: satu-satunya yang tahu bentuk keluaran untuk pengguna =====
function renderLaporan(daftar) {
const formatRp = new Intl.NumberFormat("id-ID");
const baris = ["=== Laporan Penjualan Harian ==="];
for (const item of daftar) {
baris.push(`${item.produk}: ${item.jumlah} pcs x Rp ${formatRp.format(item.harga)}`);
}
baris.push(`Terlaris: ${produkTerlaris(daftar)}`);
baris.push(`Total pendapatan: Rp ${formatRp.format(totalPendapatan(daftar))}`);
return baris.join("\n");
}
console.log(renderLaporan(penjualan));Output yang Dihasilkan:
=== Laporan Penjualan Harian ===
Pulpen Gel: 12 pcs x Rp 8.000
Buku Tulis: 20 pcs x Rp 5.500
Penggaris: 5 pcs x Rp 4.000
Terlaris: Buku Tulis
Total pendapatan: Rp 226.000Keluarannya identik persis dengan versi campur — pemisahan tidak mengubah perilaku, hanya menata tempatnya. Mari kita telaah strukturnya:
- Baris 5–9 (
const penjualan = [...]): data kini berdiri sendiri di paling atas. Ia bisa diganti dengan data dari file atau server tanpa menyentuh function mana pun. - Baris 12–18 (
totalPendapatan): logika ditulis sebagai pure function — menerimadaftar, mengembalikan angka, tanpa mencetak apa pun. Function seperti ini bisa dipanggil dari laporan mana pun, bukan hanya laporan harian. - Baris 20–28 (
produkTerlaris): aturan “terlaris” punya function sendiri. Bila kebijakannya berubah — misalnya terlaris dihitung dari pendapatan, bukan jumlah — hanya function ini yang disunting. - Baris 31–40 (
renderLaporan): tampilan memanggilprodukTerlarisdantotalPendapatan, lalu memformat hasilnya. Perhatikan bahwa lapisan tampilan boleh menggunakan logika, tetapi tidak boleh menggantikannya. - Baris 42 (
console.log(renderLaporan(penjualan))): hanya satu baris yang mencetak. Arahan alurnya jelas: data masuk ke render, render meminjam logika, logika tidak tahu siapa yang memakainya.
Manfaat yang Terasa Saat Aplikasi Berubah #
Pemisahan terasa ketika kita perlu menguji satu bagian saja. Karena totalPendapatan tidak mencetak dan tidak membaca data global, ujinya cukup satu baris. Bila baris berikut ditempel di akhir laporan-terpisah.js dan dijalankan:
console.log(totalPendapatan([])); // data kosong harus tetap aman
Output yang Dihasilkan:
0Versi campur tidak menawarkan jalan sesederhana itu: satu-satunya jalan menjalankan seluruh cetakLaporanHarian, lengkap dengan laporan yang tercetak. Contoh lain yang sering terjadi di toko Budi:
- Sumber data berubah — penjualan kini datang dari server, bukan array tertulis. Hanya lapisan data yang berubah; logika dan tampilan tidak disentuh.
- Format berubah — laporan perlu dikirim sebagai CSV untuk tim keuangan. Hanya lapisan tampilan yang berubah, dan laporan konsol lama tetap jalan.
- Kebijakan berubah — produk terlaris kini dihitung dari pendapatan. Hanya satu function logika yang disunting, dan ujinya bisa ditulis sebelum menyentuh tampilan.
Pemisahan juga membuat tiap lapisan bisa dipercaya tanpa dibaca ulang. Function totalPendapatan yang sudah diuji boleh dipanggil dari laporan harian, laporan mingguan, maupun fitur keranjang — semua pihak mendapat angka dari rumus yang sama, bukan dari salinan yang masing-masing dirawat orang berbeda. Inilah yang akhirnya menekan jumlah pekerjaan dalam jangka panjang: bukan menulis lebih cepat hari ini, tetapi lebih sedikit yang harus diingat bulan depan.
Setiap lapisan pada berkas di atas masih hidup dalam satu berkas, dan itu memang cukup untuk program sebesar ini. Pemisahan dimulai dari function, bukan dari struktur folder. Ketika aplikasi tumbuh dan lapisan-lapisan ini menuntut berkasnya masing-masing, pertanyaan berikutnya adalah bagaimana menatanya: siapa di folder mana, dan bagaimana mereka saling memanggil. Topik itu dibahas di chapter Modular Architecture.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
laporan-campur.js
Unduh berkas// laporan-campur.js — data, logika, dan tampilan bercampur dalam satu function.
// Jalankan di terminal: node laporan-campur.js
function cetakLaporanHarian() {
// Data tertanam langsung di dalam function.
const penjualan = [
{ produk: "Pulpen Gel", harga: 8000, jumlah: 12 },
{ produk: "Buku Tulis", harga: 5500, jumlah: 20 },
{ produk: "Penggaris", harga: 4000, jumlah: 5 },
];
// Penghitungan menyatu dengan pencetakan.
let total = 0;
let terlaris = penjualan[0];
for (const item of penjualan) {
total += item.harga * item.jumlah;
if (item.jumlah > terlaris.jumlah) {
terlaris = item;
}
}
// Pemformatan tampilan juga menyatu di sini.
const formatRp = new Intl.NumberFormat("id-ID");
console.log("=== Laporan Penjualan Harian ===");
for (const item of penjualan) {
console.log(`${item.produk}: ${item.jumlah} pcs x Rp ${formatRp.format(item.harga)}`);
}
console.log(`Terlaris: ${terlaris.produk}`);
console.log(`Total pendapatan: Rp ${formatRp.format(total)}`);
}
cetakLaporanHarian();
laporan-terpisah.js
Unduh berkas// laporan-terpisah.js — data, logika, dan tampilan masing-masing terpisah.
// Jalankan di terminal: node laporan-terpisah.js
// ===== Lapisan DATA: hanya menyimpan nilai, tidak menghitung dan tidak mencetak =====
const penjualan = [
{ produk: "Pulpen Gel", harga: 8000, jumlah: 12 },
{ produk: "Buku Tulis", harga: 5500, jumlah: 20 },
{ produk: "Penggaris", harga: 4000, jumlah: 5 },
];
// ===== Lapisan LOGIKA: pure function, menerima data dan mengembalikan hasil =====
function totalPendapatan(daftar) {
let total = 0;
for (const item of daftar) {
total += item.harga * item.jumlah;
}
return total;
}
function produkTerlaris(daftar) {
let terlaris = daftar[0];
for (const item of daftar) {
if (item.jumlah > terlaris.jumlah) {
terlaris = item;
}
}
return terlaris.produk;
}
// ===== Lapisan TAMPILAN: satu-satunya yang tahu bentuk keluaran untuk pengguna =====
function renderLaporan(daftar) {
const formatRp = new Intl.NumberFormat("id-ID");
const baris = ["=== Laporan Penjualan Harian ==="];
for (const item of daftar) {
baris.push(`${item.produk}: ${item.jumlah} pcs x Rp ${formatRp.format(item.harga)}`);
}
baris.push(`Terlaris: ${produkTerlaris(daftar)}`);
baris.push(`Total pendapatan: Rp ${formatRp.format(totalPendapatan(daftar))}`);
return baris.join("\n");
}
console.log(renderLaporan(penjualan));