Program yang tumbuh dalam satu berkas akhirnya mencampur segalanya, perhitungan, penyusunan teks, dan panggilan konsol, sehingga satu perubahan kecil bisa menyentuh banyak bagian sekaligus. Topik ini memecah satu program rekap nilai ujian menjadi tiga berkas modul dengan tanggung jawab yang jelas, lalu menunjukkan mengapa fungsi murni menjadi pengikatnya. Hasil akhirnya berupa kode yang tiap bagiannya dapat diuji, diganti, dan dibaca ulang tanpa menyentuh bagian lain.
Satu tanggung jawab per berkas#
Prinsipnya sederhana, satu berkas mengerjakan satu jenis pekerjaan. Berkas logika menghitung, berkas presentasi menyusun teks, dan berkas utama menghubungkan keduanya dengan dunia luar, yaitu data masukan dan panggilan console.log. Pembagian ini disebut pemisahan tanggung jawab (separation of concerns), dan modul ES memberikan wadahnya secara alami karena setiap berkas punya scope sendiri serta nilai yang diekspor secara sengaja.
Diagram berikut mengikuti arah data pada proyek mini ini. Berkas utama mengimpor fungsi hitung untuk mengolah angka mentah, lalu mengimpor penyusun teks untuk merapikan hasilnya sebelum dicetak.
Ukuran berkas bukan tujuannya, kejelasan batasnya yang penting. Ketika logika perlu berubah, misalnya rata-rata diganti median, berkas yang disentuh hanya satu, dan ketika format laporan berubah, bagian yang disunting pun berbeda lagi. Perubahan yang dulu menyebar ke mana-mana kini berhenti di batas berkasnya masing-masing, dan pembaca yang baru datang cukup membaca satu berkas untuk memahami satu pekerjaan.
Fungsi murni untuk logika inti#
Fungsi murni (pure function) menghasilkan keluaran yang hanya ditentukan masukannya, tanpa membaca atau mengubah apa pun di luar dirinya. Tidak ada console.log di dalamnya, dan tidak ada berkas yang disentuh. Memanggilnya dua kali dengan masukan yang sama selalu menghasilkan keluaran yang sama. Berkas logika proyek ini berisi empat fungsi dengan sifat itu.
// Logika inti: fungsi murni yang menghitung rekap nilai ujian.
export function rataRata(nilai) {
const total = nilai.reduce((jumlah, n) => jumlah + n, 0);
return total / nilai.length;
}
export function tertinggi(nilai) {
return Math.max(...nilai);
}
export function terendah(nilai) {
return Math.min(...nilai);
}
export function jumlahLulus(nilai, batas) {
return nilai.filter((n) => n >= batas).length;
}Sifat murni itulah yang membuat keempat fungsi tersebut nyaris gratis untuk diuji. Kasus ujinya cukup memanggil fungsi dengan sebuah array lalu membandingkan hasilnya, tanpa perlu menggantikan konsol, berkas, atau jaringan apa pun, persis seperti pola susun-eksekusi-periksa pada unit test. Fungsi yang bergantung pada keadaan luar menuntut persiapan jauh lebih banyak sebelum bisa diuji, sehingga usaha menaruh logika inti ke fungsi murni langsung terbayar saat pengujian.
Presentasi yang terpisah dari perhitungan#
Format rekap, lebar kolom, jumlah desimal, dan labelnya adalah keputusan presentasi yang berubah jauh lebih sering daripada rumusnya. Dengan menaruhnya di berkas tersendiri, perubahan tampilan laporan tidak pernah menyentuh perhitungan, dan sebaliknya. Berkas format.mjs menerima objek rekap yang angkanya sudah jadi, lalu tugasnya hanya menyusun string.
// Lapisan presentasi: menyusun angka yang sudah jadi menjadi teks rekap.
export function formatRekap(rekap) {
return [
`Rekap nilai ujian ${rekap.judul}`,
`jumlah peserta : ${rekap.total}`,
`rata-rata : ${rekap.rataRata.toFixed(1)}`,
`nilai tertinggi : ${rekap.tertinggi}`,
`nilai terendah : ${rekap.terendah}`,
`lulus : ${rekap.lulus} dari ${rekap.total} peserta`,
].join("\n");
}Fungsinya tetap murni, masukan menentukan keluaran dan tidak ada efek samping, hanya saja domainnya teks alih-alih angka. Pola ini juga membuka pilihan tanpa biaya tambahan. Bila laporan berikutnya butuh bentuk CSV atau HTML, cukup tambahkan fungsi penyusun baru di berkas yang sama, sementara hitung.mjs dan utama.mjs tidak berubah sama sekali. Pemisahan yang tadi terasa formal kini terlihat manfaatnya, bagian yang paling sering berubah dikumpulkan di satu tempat.
Merangkai lapisan di utama.mjs#
Berkas utama sengaja dibuat tipis, tugasnya hanya tiga, menyiapkan data, menyusun objek rekap dengan fungsi dari hitung.mjs, dan mencetak hasil penyusunan format.mjs. Tidak ada perhitungan atau pemformatan yang ditulis ulang di sini, sehingga berkas ini menjadi tempat satu-satunya yang perlu disentuh ketika sumber datanya berubah.
// tidak-dijalankan
// Lapisan I/O: data, perakitan rekap, dan penampilan hasil.
import { rataRata, tertinggi, terendah, jumlahLulus } from "./hitung.mjs";
import { formatRekap } from "./format.mjs";
const nilaiUjian = [78, 92, 65, 88, 74, 95, 81];
const batasLulus = 75;
const rekap = {
judul: "kelas 8B",
total: nilaiUjian.length,
rataRata: rataRata(nilaiUjian),
tertinggi: tertinggi(nilaiUjian),
terendah: terendah(nilaiUjian),
lulus: jumlahLulus(nilaiUjian, batasLulus),
};
console.log(formatRekap(rekap));# jalankan proyek mini dari berkas utamanya
node utama.mjsRekap nilai ujian kelas 8B
jumlah peserta : 7
rata-rata : 81.9
nilai tertinggi : 95
nilai terendah : 65
lulus : 5 dari 7 pesertaKeluaran itu lahir dari kerja tiga berkas, tetapi tidak ada satu pun di antaranya yang tahu cara kerja yang lain. hitung.mjs tidak tahu nilainya dicetak, format.mjs tidak tahu dari mana angkanya berasal, dan utama.mjs tidak tahu rumus di balik angka itu. Blok utama.mjs sendiri ditandai tidak-dijalankan karena skrip itu mengimpor dua modul lain, sehingga perintahnya baru bisa dijalankan setelah ketiga berkas berada di satu folder yang sama.
Kesalahan umum#
Tiga kekeliruan berikut paling sering muncul saat proyek mulai dipecah menjadi modul, dan semuanya berkaitan dengan batas antarberkas.
- Menaruh efek samping di modul yang diimpor berkas lain, misalnya
console.logatau pembacaan berkas di tingkat puncakhitung.mjs. Modul semacam itu sulit diuji karena mengimpornya saja sudah mencetak. Simpan efek samping di lapisan utama, sedangkan modul lain cukup mengekspor fungsi. - Impor yang saling menunggu, ketika
format.mjsmengimpor dariutama.mjspadahalutama.mjsmengimpor dariformat.mjs. Rantai melingkar semacam ini mengaburkan arah data dan sebaiknya dipatahkan dengan menaruh nilai bersama di modul ketiga. - Satu berkas membesar tanpa batas karena “cukup satu fungsi lagi” terus ditambahkan. Bila berkas logika mulai berisi penyusunan teks, pindahkan bagian itu ke berkas penyusunnya, karena batas tanggung jawab lebih murah dipelihara daripada dibangun ulang.
Keluhan yang paling sering muncul adalah fungsi yang sulit diuji tanpa alasan jelas. Ketika itu terjadi, telusuri mundur dari fungsi tersebut, biasanya ditemukan efek samping, keadaan luar, atau dua tanggung jawab yang menyatu di satu berkas. Ketiga penyebab itu hanya bisa dibereskan dengan memindahkan kode ke berkas yang memang seharusnya menangani tugas tersebut.
Ringkasan#
- Pemisahan tanggung jawab menaruh logika, presentasi, dan I/O pada berkas modul yang berbeda.
- Fungsi murni hanya bergantung pada masukannya, sehingga paling murah untuk diuji dan paling aman untuk diganti.
- Lapisan presentasi berubah paling sering, sehingga dipisahkan dari perhitungan yang lebih stabil.
- Berkas utama hanya merangkai lapisan-lapisan itu, data masuk, rekap disusun, hasil dicetak.
Referensi#
- JavaScript modules — MDN Web Docs — panduan modul ES sebagai wadah pemisahan kode antarberkas.
- ECMAScript Modules — Node.js — dukungan ESM di Node.js, termasuk aturan resolusi jalurnya.