Bab 10 dari Eloquent JavaScript. Sebelumnya: 09 Regular Expression. Berikutnya: 11 Pemrograman Asinkron.
Tulis kode yang gampang dihapus, bukan gampang diperluas.
— Tef, programming is terrible

Idealnya, program punya struktur yang jelas dan lugas. Cara kerjanya gampang dijelasin, dan setiap bagian mainin peran yang terdefinisi dengan baik.
Dalam praktik, program tumbuh secara organik. Potongan fungsionalitas ditambahin seiring programmer ngenalin kebutuhan baru. Ngejaga program kaya gitu tetap terstruktur dengan baik butuh perhatian dan kerja terus-menerus. Ini kerja yang cuma bakal terbayar di masa depan, waktu berikutnya seseorang ngerjain programnya, jadi menggoda buat ngabaikannya dan ngebiarin berbagai bagian program jadi kusut parah.
Ini bikin dua masalah praktis. Pertama, memahami sistem yang kusut itu susah. Kalau semuanya bisa nyentuh semuanya, susah buat ngelihat satu bagian secara terisolasi. Kamu dipaksa ngebangun pemahaman holistik atas keseluruhannya. Kedua, kalau kamu mau pakai fungsionalitas apa pun dari program kaya gitu di situasi lain, nulis ulang mungkin lebih gampang daripada nyoba nguraikannya dari konteksnya.
Frasa “big ball of mud” (bola lumpur besar) sering dipakai buat program besar tanpa struktur kaya gitu. Semuanya nempel jadi satu, dan waktu kamu nyoba ngambil satu bagian, seluruhnya berantakan, dan kamu cuma berhasil bikin kekacauan.
Program Modular
Modul adalah usaha buat ngehindarin masalah-masalah ini. Modul adalah potongan program yang nentuin potongan lain mana yang dia andalkan dan fungsionalitas mana yang dia sediain buat dipakai modul lain (interface-nya).
Interface modul punya banyak kesamaan dengan interface objek, kaya yang kita lihat di Bab 6. Mereka bikin sebagian modul tersedia buat dunia luar dan ngejaga sisanya privat.
Tapi interface yang disediain modul buat dipakai orang lain cuma setengah cerita. Sistem modul yang bagus juga ngeharusin modul nentuin kode mana yang mereka pakai dari modul lain. Hubungan ini disebut dependency (ketergantungan). Kalau modul A pakai fungsionalitas dari modul B, dia dibilang bergantung pada modul itu. Waktu ini ditentuin dengan jelas di modulnya sendiri, mereka bisa dipakai buat nyari tahu modul lain mana yang perlu ada biar bisa pakai modul tertentu dan buat otomatis ngeload dependency.
Waktu cara modul berinteraksi satu sama lain eksplisit, sistem jadi lebih kaya LEGO, di mana potongan-potongan berinteraksi lewat konektor yang terdefinisi dengan baik, dan kurang kaya lumpur, di mana semuanya bercampur dengan semuanya.
ES Modules
Bahasa JavaScript asli gak punya konsep modul sama sekali. Semua script jalan di scope yang sama, dan ngakses fungsi yang didefinisikan di script lain dilakuin dengan ngerujuk binding global yang dibikin script itu. Ini secara aktif mendorong kekusutan kode yang gak sengaja dan susah dilihat dan ngundang masalah kaya script yang gak berhubungan nyoba pakai nama binding yang sama.
Sejak ECMAScript 2015, JavaScript mendukung dua jenis program berbeda. Script berperilaku dengan cara lama: bindingnya didefinisikan di scope global, dan mereka gak punya cara buat langsung ngerujuk script lain. Modul dapet scope terpisah sendiri dan mendukung keyword import dan export, yang gak tersedia di script, buat mendeklarasikan dependency dan interface mereka. Sistem modul ini biasanya disebut ES modules (di mana ES singkatan dari ECMAScript).
Program modular tersusun dari sejumlah modul kaya gitu, dirangkai lewat import dan export mereka.
Modul contoh berikut ngonversi antara nama hari dan angka (kaya yang dikembaliin method getDay dari Date). Dia mendefinisikan konstanta yang bukan bagian dari interface-nya, dan dua fungsi yang iya. Dia gak punya dependency.
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
export function dayName(number) {
return names[number];
}
export function dayNumber(name) {
return names.indexOf(name);
}Keyword export bisa ditaruh di depan definisi fungsi, class, atau binding buat nunjukin bahwa binding itu bagian dari interface modul. Ini bikin mungkin buat modul lain pakai binding itu dengan ngimpornya.
import {dayName} from "./dayname.js";
let now = new Date();
console.log(`Today is ${dayName(now.getDay())}`);
// → Today is MondayKeyword import, diikuti daftar nama binding dalam kurung kurawal, bikin binding dari modul lain tersedia di modul saat ini. Modul diidentifikasi dengan string berkutip.
Cara nama modul kaya gitu diselesaikan ke program sebenarnya beda antar platform. Browser memperlakukannya sebagai alamat web, sedangkan Node.js nyelesaiinnya ke file. Waktu kamu ngejalanin modul, semua modul lain yang dia andalkan, dan modul yang mereka andalkan, di-load, dan binding yang diekspor dibikin tersedia buat modul yang ngimpornya.
Deklarasi import dan export gak bisa muncul di dalam fungsi, loop, atau block lain. Mereka langsung diselesaikan waktu modulnya di-load, terlepas dari gimana kode di modulnya dieksekusi. Buat nyerminin ini, mereka cuma boleh muncul di badan modul terluar.
Interface modul jadi terdiri dari kumpulan binding bernama, yang bisa diakses modul lain yang bergantung pada modul itu. Binding yang diimpor bisa diganti namanya buat ngasih nama lokal baru pakai as setelah namanya.
import {dayName as nomDeJour} from "./dayname.js";
console.log(nomDeJour(3));
// → WednesdayModul juga bisa punya export khusus bernama default, yang sering dipakai buat modul yang cuma ngekspor satu binding. Buat mendefinisikan default export, kamu nulis export default sebelum ekspresi, deklarasi fungsi, atau deklarasi class.
export default ["Winter", "Spring", "Summer", "Autumn"];Binding kaya gitu diimpor dengan ngilangin kurung kurawal di sekitar nama importnya.
import seasonNames from "./seasonname.js";Buat ngimpor semua binding dari modul sekaligus, kamu bisa pakai import *. Kamu nyediain nama, dan nama itu bakal terikat ke objek yang megang semua export modulnya. Ini bisa berguna waktu kamu pakai banyak export berbeda.
import * as dayName from "./dayname.js";
console.log(dayName.dayName(3));
// → WednesdayPackage
Salah satu keuntungan ngebangun program dari potongan terpisah dan bisa ngejalanin beberapa potongan itu sendiri adalah kamu mungkin bisa pakai potongan yang sama di program berbeda.
Tapi gimana cara nyiapin ini? Misalnya saya mau pakai fungsi parseINI dari Bab 9 di program lain. Kalau jelas apa yang diandalkan fungsinya (di kasus ini, gak ada), saya bisa cuma nyalin modul itu ke proyek baru saya dan pakainya. Tapi lalu, kalau saya nemuin kesalahan di kodenya, saya mungkin bakal memperbaikinya di program mana pun yang lagi saya kerjain saat itu dan lupa buat juga memperbaikinya di program lain.
Begitu kamu mulai nduplikasi kode, kamu bakal cepat nemuin dirimu buang waktu dan energi mindahin salinan ke sana kemari dan ngejaganya tetap up to date. Di sinilah package masuk. Package adalah potongan kode yang bisa didistribusikan (disalin dan diinstal). Dia mungkin berisi satu modul atau lebih dan punya informasi soal package lain mana yang dia andalkan. Package juga biasanya datang dengan dokumentasi yang ngejelasin apa yang dia lakuin biar orang yang gak nulisnya masih bisa pakainya.
Waktu masalah ditemuin di package atau fitur baru ditambahin, package-nya diupdate. Sekarang program yang bergantung padanya (yang mungkin juga package) bisa nyalin versi barunya buat dapet perbaikan yang dibikin ke kodenya.
Kerja dengan cara ini butuh infrastruktur. Kita butuh tempat buat nyimpen dan nemuin package dan cara praktis buat nginstal dan ngupgrade mereka. Di dunia JavaScript, infrastruktur ini disediain sama NPM (https://npmjs.com).
NPM itu dua hal: layanan online tempat kamu bisa ngunduh (dan ngunggah) package, dan program (dibundel dengan Node.js) yang ngebantu kamu nginstal dan ngelola mereka.
Saat tulisan ini dibuat, ada lebih dari tiga juta package berbeda yang tersedia di NPM. Sebagian besar dari itu sampah, jujur aja. Tapi hampir setiap package JavaScript yang berguna dan tersedia publik bisa ditemuin di NPM. Misalnya, parser file INI, mirip yang kita bangun di Bab 9, tersedia dengan nama package ini.
Bab 20 bakal nunjukin cara nginstal package kaya gitu secara lokal pakai program command line npm.
Punya package berkualitas yang tersedia buat diunduh itu sangat berharga. Itu berarti kita sering bisa ngehindarin nemuin ulang program yang udah ditulis 100 orang sebelumnya dan dapet implementasi yang solid dan teruji dengan mencet beberapa tombol.
Software murah buat disalin, jadi begitu seseorang udah nulisnya, mendistribusikannya ke orang lain adalah proses yang efisien. Nulisnya di awal emang kerjaan, dan merespons orang yang nemuin masalah di kodenya atau yang mau ngusulin fitur baru bahkan lebih banyak kerjaan.
Secara default, kamu punya hak cipta atas kode yang kamu tulis, dan orang lain cuma boleh pakainya dengan izinmu. Tapi karena beberapa orang emang baik dan karena nerbitin software yang bagus bisa bikin kamu sedikit terkenal di kalangan programmer, banyak package diterbitkan di bawah lisensi yang secara eksplisit ngebolehin orang lain pakainya.
Kebanyakan kode di NPM dilisensikan dengan cara ini. Beberapa lisensi ngeharusin kamu juga nerbitin kode yang kamu bangun di atas package-nya di bawah lisensi yang sama. Yang lain kurang menuntut, cuma ngeharusin kamu nyimpen lisensinya bareng kodenya waktu kamu mendistribusikannya. Komunitas JavaScript kebanyakan pakai jenis lisensi yang kedua. Waktu pakai package orang lain, pastiin kamu sadar sama lisensinya.
Sekarang, bukannya nulis parser file INI kita sendiri, kita bisa pakai yang dari NPM.
import {parse} from "ini";
console.log(parse("x = 10\ny = 20"));
// → {x: "10", y: "20"}Modul CommonJS
Sebelum 2015, waktu bahasa JavaScript gak punya sistem modul bawaan, orang-orang udah ngebangun sistem besar di JavaScript. Biar itu bisa dikerjain, mereka butuh modul.
Komunitasnya ngedesain sistem modul improvisasi mereka sendiri di atas bahasanya. Ini pakai fungsi buat bikin scope lokal buat modul dan objek biasa buat mewakili interface modul.
Awalnya, orang cuma secara manual ngebungkus seluruh modul mereka dalam “immediately invoked function expression” (ekspresi fungsi yang langsung dipanggil) buat bikin scope modulnya dan nge-assign objek interface mereka ke satu variabel global.
const weekDay = function() {
const names = ["Sunday", "Monday", "Tuesday", "Wednesday",
"Thursday", "Friday", "Saturday"];
return {
name(number) { return names[number]; },
number(name) { return names.indexOf(name); }
};
}();
console.log(weekDay.name(weekDay.number("Sunday")));
// → SundayGaya modul ini nyediain isolasi, sampai tingkat tertentu, tapi gak mendeklarasikan dependency. Sebagai gantinya, dia cuma naruh interface-nya ke scope global dan ngarepin dependency-nya, kalau ada, ngelakuin hal yang sama. Ini gak ideal.
Kalau kita ngimplementasiin module loader kita sendiri, kita bisa lebih baik. Pendekatan yang paling banyak dipakai buat modul JavaScript tambahan disebut modul CommonJS. Node.js pakai sistem modul ini dari awal (walaupun sekarang dia juga tahu cara ngeload ES modules), dan ini sistem modul yang dipakai banyak package di NPM.
Modul CommonJS keliatan kaya script biasa, tapi dia punya akses ke dua binding yang dia pakai buat berinteraksi sama modul lain. Yang pertama adalah fungsi bernama require. Waktu kamu manggil ini dengan nama modul dependency-mu, dia mastiin modulnya di-load dan ngembaliin interface-nya. Yang kedua adalah objek bernama exports, yang merupakan objek interface buat modulnya. Dia mulai kosong dan kamu nambahin properti ke dia buat mendefinisikan nilai yang diekspor.
Modul contoh CommonJS ini nyediain fungsi format tanggal. Dia pakai dua package dari NPM, ordinal buat ngonversi angka ke string kaya "1st" dan "2nd", dan date-names buat dapet nama Inggris buat hari kerja dan bulan. Dia ngekspor satu fungsi, formatDate, yang ngambil objek Date dan string template.
String template-nya bisa berisi kode yang ngarahin formatnya, kaya YYYY buat tahun penuh dan Do buat hari ordinal dalam bulan. Kamu bisa ngasihnya string kaya "MMMM Do YYYY" buat dapet output kaya November 22nd 2017.
const ordinal = require("ordinal");
const {days, months} = require("date-names");
exports.formatDate = function(date, format) {
return format.replace(/YYYY|M(MMM)?|Do?|dddd/g, tag => {
if (tag == "YYYY") return date.getFullYear();
if (tag == "M") return date.getMonth();
if (tag == "MMMM") return months[date.getMonth()];
if (tag == "D") return date.getDate();
if (tag == "Do") return ordinal(date.getDate());
if (tag == "dddd") return days[date.getDay()];
});
};Interface ordinal adalah satu fungsi, sedangkan date-names ngekspor objek yang berisi beberapa hal, days dan months adalah array nama. Destructuring sangat praktis waktu bikin binding buat interface yang diimpor.
Modulnya nambahin fungsi interface-nya ke exports biar modul yang bergantung padanya dapet akses. Kita bisa pakai modulnya kaya gini:
const {formatDate} = require("./format-date.js");
console.log(formatDate(new Date(2017, 9, 13),
"dddd the Do"));
// → Friday the 13thCommonJS diimplementasiin dengan module loader yang, waktu ngeload modul, ngebungkus kodenya dalam fungsi (ngasihnya scope lokal sendiri) dan ngasih binding require dan exports ke fungsi itu sebagai argumen.
Kalau kita anggap kita punya akses ke fungsi readFile yang baca file berdasarkan nama dan ngasih kita isinya, kita bisa mendefinisikan bentuk sederhana require kaya gini:
function require(name) {
if (!(name in require.cache)) {
let code = readFile(name);
let exports = require.cache[name] = {};
let wrapper = Function("require, exports", code);
wrapper(require, exports);
}
return require.cache[name];
}
require.cache = Object.create(null);Function adalah fungsi JavaScript bawaan yang ngambil daftar argumen (sebagai string yang dipisah koma) dan string yang berisi badan fungsi dan ngembaliin nilai fungsi dengan argumen dan badan itu. Ini konsep yang menarik, dia ngebolehin program bikin potongan program baru dari data string, tapi juga berbahaya, karena kalau seseorang bisa nipu programmu buat naruh string yang mereka sediain ke Function, mereka bisa bikin programnya ngelakuin apa pun yang mereka mau.
JavaScript standar gak nyediain fungsi kaya readFile, tapi environment JavaScript yang berbeda, kaya browser dan Node.js, nyediain cara mereka sendiri buat ngakses file. Contohnya cuma pura-pura readFile ada.
Buat ngehindarin ngeload modul yang sama beberapa kali, require nyimpen penyimpanan (cache) modul yang udah di-load. Waktu dipanggil, dia pertama ngecek apakah modul yang diminta udah di-load dan, kalau belum, ngeloadnya. Ini melibatkan baca kode modulnya, ngebungkusnya dalam fungsi, dan manggilnya.
Dengan mendefinisikan require dan exports sebagai parameter buat fungsi pembungkus yang dihasilin (dan ngasih nilai yang sesuai waktu manggilnya), loader-nya mastiin binding-binding ini tersedia di scope modulnya.
Perbedaan penting antara sistem ini dan ES modules adalah import ES module terjadi sebelum script modul mulai jalan, sedangkan require adalah fungsi normal, dipanggil waktu modulnya udah jalan. Beda dengan deklarasi import, pemanggilan require bisa muncul di dalam fungsi, dan nama dependency-nya bisa ekspresi apa pun yang dievaluasi jadi string, sedangkan import cuma ngebolehin string berkutip biasa.
Transisi komunitas JavaScript dari gaya CommonJS ke ES modules udah lambat dan agak kasar. Untungnya kita sekarang ada di titik di mana kebanyakan package populer di NPM nyediain kodenya sebagai ES modules, dan Node.js ngebolehin ES modules ngimpor dari modul CommonJS. Walaupun kode CommonJS masih sesuatu yang bakal kamu temuin, gak ada alasan nyata buat nulis program baru dengan gaya ini lagi.
Building dan Bundling
Banyak package JavaScript secara teknis gak ditulis di JavaScript. Ekstensi bahasa kaya TypeScript, dialek pengecekan tipe yang disebutin di Bab 8, dipakai luas. Orang juga sering mulai pakai fitur bahasa baru yang direncanain jauh sebelum mereka ditambahin ke platform yang beneran ngejalanin JavaScript. Biar ini mungkin, mereka ngompilasi kode mereka, nerjemahinnya dari dialek JavaScript pilihan mereka ke JavaScript biasa, atau bahkan ke versi JavaScript yang lalu, biar browser bisa ngejalaninnya.
Nyertain program modular yang terdiri dari 200 file berbeda di halaman web ngehasilin masalahnya sendiri. Kalau ngambil satu file lewat jaringan butuh 50 milidetik, ngeload seluruh program butuh 10 detik, atau mungkin setengahnya kalau kamu bisa ngeload beberapa file sekaligus. Itu banyak waktu terbuang. Karena ngambil satu file besar cenderung lebih cepat daripada ngambil banyak file kecil, programmer web mulai pakai alat yang ngegabungin program mereka (yang susah payah mereka pecah jadi modul) jadi satu file besar sebelum mereka nerbitinnya ke web. Alat kaya gitu disebut bundler.
Dan kita bisa lebih jauh lagi. Selain jumlah file, ukuran file juga nentuin seberapa cepat mereka bisa ditransfer lewat jaringan. Jadi, komunitas JavaScript nyiptain minifier. Ini alat yang ngambil program JavaScript dan bikinnya lebih kecil dengan otomatis ngehapus komentar dan whitespace, ngeganti nama binding, dan ngegantiin potongan kode dengan kode setara yang makan lebih sedikit ruang.
Gak jarang kode yang kamu temuin di package NPM atau yang jalan di halaman web udah lewat beberapa tahap transformasi, ngonversi dari JavaScript modern ke JavaScript lama, ngegabungin modul jadi satu file, dan meminifikasi kodenya. Kita gak bakal masuk ke detail alat-alat ini di buku ini, karena ada banyak, dan mana yang populer berubah secara teratur. Cukup sadar bahwa hal kaya gitu ada, dan cari waktu kamu butuh.
Desain Modul
Nyusun program adalah salah satu aspek yang lebih halus dari pemrograman. Potongan fungsionalitas nontrivial apa pun bisa diorganisir dengan berbagai cara.
Desain program yang bagus itu subjektif, ada trade-off yang terlibat, dan soal selera. Cara terbaik buat belajar nilai dari desain yang terstruktur dengan baik adalah baca atau ngerjain banyak program dan merhatiin apa yang jalan dan apa yang gak. Jangan anggap kekacauan yang menyakitkan itu “emang begitu adanya”. Kamu bisa memperbaiki struktur hampir semua hal dengan lebih banyak mikirin.
Salah satu aspek desain modul adalah kemudahan penggunaan. Kalau kamu ngedesain sesuatu yang dimaksudkan buat dipakai banyak orang, atau bahkan dirimu sendiri, tiga bulan lagi waktu kamu udah gak ingat detail spesifik apa yang kamu lakuin, membantu kalau interface-mu sederhana dan bisa ditebak.
Itu mungkin berarti ngikutin konvensi yang udah ada. Contoh bagusnya adalah package ini. Modul ini niru objek JSON standar dengan nyediain fungsi parse dan stringify (buat nulis file INI), dan, kaya JSON, ngonversi antara string dan objek biasa. Interface-nya kecil dan akrab, dan setelah kamu pakainya sekali, kamu kemungkinan ingat cara pakainya.
Bahkan kalau gak ada fungsi standar atau package yang dipakai luas buat ditiru, kamu bisa ngejaga modulmu bisa ditebak dengan pakai struktur data sederhana dan ngelakuin satu hal yang fokus. Banyak modul parsing file INI di NPM nyediain fungsi yang langsung baca file kaya gitu dari hard disk dan mem-parse-nya, misalnya. Ini bikin gak mungkin pakai modul kaya gitu di browser, di mana kita gak punya akses filesystem langsung, dan nambahin kerumitan yang bakal lebih baik diatasi dengan ngomposisi modulnya dengan suatu fungsi baca file.
Ini nunjuk ke aspek lain yang membantu dari desain modul, kemudahan sesuatu bisa dikomposisi dengan kode lain. Modul fokus yang ngitung nilai bisa diterapin di lebih banyak program daripada modul lebih besar yang ngelakuin aksi rumit dengan side effect. Pembaca file INI yang ngotot baca filenya dari disk gak berguna di skenario di mana isi filenya datang dari sumber lain.
Terkait, objek yang punya state kadang berguna atau bahkan perlu, tapi kalau sesuatu bisa dilakuin dengan fungsi, pakai fungsi. Beberapa pembaca file INI di NPM nyediain gaya interface yang ngeharusin kamu pertama bikin objek, lalu ngeload filenya ke objekmu, dan terakhir pakai method khusus buat ngambil hasilnya. Hal jenis ini umum di tradisi berorientasi objek, dan ini mengerikan. Bukannya bikin satu pemanggilan fungsi dan lanjut, kamu harus ngelakuin ritual mindahin objekmu lewat berbagai keadaannya. Dan karena datanya sekarang dibungkus dalam tipe objek khusus, semua kode yang berinteraksi dengannya harus tahu soal tipe itu, bikin saling ketergantungan yang gak perlu.
Sering kali, mendefinisikan struktur data baru gak bisa dihindari, cuma beberapa yang dasar yang disediain standar bahasa, dan banyak tipe data harus lebih kompleks dari array atau map. Tapi waktu array cukup, pakai array.
Contoh struktur data yang sedikit lebih kompleks adalah graph dari Bab 7. Gak ada satu cara jelas buat mewakili graph di JavaScript. Di bab itu, kita pakai objek yang propertinya megang array string, node lain yang bisa dicapai dari node itu.
Ada beberapa package pathfinding berbeda di NPM, tapi gak ada yang pakai format graph ini. Mereka biasanya ngebolehin edge graph punya bobot, yaitu biaya atau jarak yang terkait dengannya. Itu gak mungkin di representasi kita.
Misalnya, ada package dijkstrajs. Pendekatan terkenal buat pathfinding, cukup mirip dengan fungsi findRoute kita, disebut algoritma Dijkstra, dari nama Edsger Dijkstra, yang pertama nulisnya. Akhiran js sering ditambahin ke nama package buat nunjukin fakta bahwa mereka ditulis di JavaScript. Package dijkstrajs ini pakai format graph yang mirip dengan kita, tapi bukannya array, dia pakai objek yang nilai propertinya angka, bobot dari edge-nya.
Kalau kita mau pakai package itu, kita harus mastiin graph kita disimpan dalam format yang dia harapkan. Semua edge dapet bobot yang sama, karena model sederhana kita memperlakukan setiap jalan punya biaya yang sama (satu giliran).
const {find_path} = require("dijkstrajs");
let graph = {};
for (let node of Object.keys(roadGraph)) {
let edges = graph[node] = {};
for (let dest of roadGraph[node]) {
edges[dest] = 1;
}
}
console.log(find_path(graph, "Post Office", "Cabin"));
// → ["Post Office", "Alice's House", "Cabin"]Ini bisa jadi penghalang komposisi, waktu berbagai package pakai struktur data berbeda buat menggambarkan hal serupa, ngegabunginnya susah. Makanya, kalau kamu mau ngedesain buat komposabilitas, cari tahu struktur data apa yang dipakai orang lain dan, kalau mungkin, ikutin contoh mereka.
Ngedesain struktur modul yang pas buat program bisa susah. Di fase di mana kamu masih ngeksplorasi masalahnya, nyoba berbagai hal buat lihat apa yang jalan, kamu mungkin mau gak terlalu khawatir soal itu, karena ngejaga semuanya terorganisir bisa jadi gangguan besar. Begitu kamu punya sesuatu yang terasa solid, itu saat yang bagus buat mundur selangkah dan ngorganisirnya.
Ringkasan
Modul nyediain struktur buat program yang lebih besar dengan misahin kode jadi potongan-potongan dengan interface dan dependency yang jelas. Interface adalah bagian modul yang terlihat buat modul lain, dan dependency adalah modul lain yang dia manfaatin.
Karena JavaScript secara historis gak nyediain sistem modul, sistem CommonJS dibangun di atasnya. Lalu di suatu titik dia emang dapet sistem bawaan, yang sekarang hidup berdampingan dengan canggung sama sistem CommonJS.
Package adalah potongan kode yang bisa didistribusikan sendiri. NPM adalah repositori package JavaScript. Kamu bisa ngunduh segala macam package yang berguna (dan gak berguna) dari sana.
Poin-poin pentingnya:
- Modul = potongan program dengan interface (yang diekspor) dan dependency (yang diimpor) yang jelas, biar program gak jadi “bola lumpur”.
- ES modules:
exportdi depan definisi,import {x} from "./file.js",import {x as y},export default,import * as ns. Harus di level teratas modul. - Package = kode yang bisa didistribusikan, disimpan di NPM. Perhatiin lisensinya.
- CommonJS (sistem lama Node.js):
require("nama")danexports.x = .... Bisa dipanggil di mana aja, tapi buat program baru pakai ES modules. - Bundler ngegabungin banyak file jadi satu, minifier ngecilin ukurannya.
- Desain modul yang baik: interface sederhana dan bisa ditebak, pakai struktur data sederhana, fokus ke satu hal, lebih pilih fungsi daripada objek ber-state, dan ikuti format data yang udah umum biar gampang dikomposisi.
Latihan
Robot Modular
Ini binding-binding yang dibikin proyek dari Bab 7:
roads
buildGraph
roadGraph
VillageState
runRobot
randomPick
randomRobot
mailRoute
routeRobot
findRoute
goalOrientedRobotKalau kamu mau nulis proyek itu sebagai program modular, modul apa yang bakal kamu bikin? Modul mana yang bakal bergantung pada modul lain mana, dan interface mereka bakal keliatan kaya apa?
Bagian mana yang kemungkinan udah tersedia di NPM? Kamu lebih suka pakai package NPM atau nulisnya sendiri?
Petunjuk:
Ini yang bakal saya lakuin (tapi lagi-lagi, gak ada satu cara benar buat ngedesain modul tertentu):
Kode yang dipakai buat ngebangun graph jalan tinggal di modul graph.js. Karena saya lebih suka pakai dijkstrajs dari NPM daripada kode pathfinding kita sendiri, kita bakal bikin ini ngebangun jenis data graph yang diharapkan dijkstrajs. Modul ini ngekspor satu fungsi, buildGraph. Saya bakal bikin buildGraph nerima array dari array dua elemen, bukannya string yang berisi tanda hubung, biar modulnya kurang bergantung pada format input.
Modul roads.js berisi data jalan mentah (array roads) dan binding roadGraph. Modul ini bergantung pada ./graph.js dan ngekspor graph jalannya.
Class VillageState tinggal di modul state.js. Dia bergantung pada modul ./roads.js karena dia perlu bisa memverifikasi bahwa jalan tertentu ada. Dia juga butuh randomPick. Karena itu fungsi tiga baris, kita bisa cuma naruhnya ke modul state.js sebagai fungsi pembantu internal. Tapi randomRobot juga butuh dia. Jadi kita harus nduplikasinya atau naruhnya di modulnya sendiri. Karena fungsi ini kebetulan ada di NPM di package random-item, solusi yang masuk akal adalah cuma bikin kedua modul bergantung pada itu. Kita bisa nambahin fungsi runRobot ke modul ini juga, karena dia kecil dan erat kaitannya dengan manajemen state. Modulnya ngekspor class VillageState dan fungsi runRobot.
Terakhir, robot-robotnya, bareng nilai-nilai yang mereka andalkan, kaya mailRoute, bisa masuk ke modul example-robots.js, yang bergantung pada ./roads.js dan ngekspor fungsi-fungsi robot. Biar goalOrientedRobot bisa nyari rute, modul ini juga bergantung pada dijkstrajs.
Dengan mindahin sebagian kerjaan ke modul NPM, kodenya jadi sedikit lebih kecil. Setiap modul individual ngelakuin sesuatu yang agak sederhana dan bisa dibaca sendiri. Ngebagi kode jadi modul juga sering nyaranin perbaikan lebih lanjut ke desain program. Di kasus ini, keliatan agak aneh bahwa VillageState dan robot-robotnya bergantung pada graph jalan tertentu. Mungkin ide yang lebih baik buat bikin graph-nya jadi argumen ke constructor state dan bikin robot-robotnya bacanya dari objek state, ini ngurangin dependency (yang selalu bagus) dan bikin mungkin ngejalanin simulasi di peta yang berbeda (yang lebih bagus lagi).
Apakah ide bagus pakai modul NPM buat hal yang bisa kita tulis sendiri? Pada prinsipnya, iya, buat hal nontrivial kaya fungsi pathfinding kamu kemungkinan bikin kesalahan dan buang waktu nulisnya sendiri. Buat fungsi kecil kaya random-item, nulisnya sendiri cukup gampang. Tapi nambahinnya di mana pun kamu butuh emang cenderung bikin modulmu berantakan.
Tapi, kamu juga gak boleh meremehkan kerja yang terlibat dalam nemuin package NPM yang sesuai. Dan bahkan kalau kamu nemuin satu, dia mungkin gak jalan dengan baik atau mungkin kurang fitur yang kamu butuhin. Di atas itu, bergantung pada package NPM berarti kamu harus mastiin mereka terinstal, kamu harus mendistribusikannya bareng programmu, dan kamu mungkin harus ngupgrade mereka secara berkala.
Jadi lagi-lagi, ini trade-off, dan kamu bisa mutusin salah satunya tergantung seberapa banyak package tertentu beneran ngebantu kamu.
Modul Roads
Tulis ES module berdasarkan contoh dari Bab 7 yang berisi array jalan dan ngekspor struktur data graph yang mewakilinya sebagai roadGraph. Dia bergantung pada modul ./graph.js yang ngekspor fungsi buildGraph, yang dipakai buat ngebangun graph-nya. Fungsi ini ngarepin array dari array dua elemen (titik awal dan akhir jalannya).
// Tambahin dependency dan export
const roads = [
"Alice's House-Bob's House", "Alice's House-Cabin",
"Alice's House-Post Office", "Bob's House-Town Hall",
"Daria's House-Ernie's House", "Daria's House-Town Hall",
"Ernie's House-Grete's House", "Grete's House-Farm",
"Grete's House-Shop", "Marketplace-Farm",
"Marketplace-Post Office", "Marketplace-Shop",
"Marketplace-Town Hall", "Shop-Town Hall"
];Petunjuk:
Karena ini ES module, kamu harus pakai import buat ngakses modul graph-nya. Itu digambarkan sebagai ngekspor fungsi buildGraph, yang bisa kamu ambil dari objek interface-nya dengan deklarasi const destructuring.
Buat ngekspor roadGraph, kamu naruh keyword export sebelum definisinya. Karena buildGraph ngambil struktur data yang gak persis cocok dengan roads, pemecahan string jalannya harus terjadi di modulmu.
Solusi:
import {buildGraph} from "./graph";
const roads = [
"Alice's House-Bob's House", "Alice's House-Cabin",
"Alice's House-Post Office", "Bob's House-Town Hall",
"Daria's House-Ernie's House", "Daria's House-Town Hall",
"Ernie's House-Grete's House", "Grete's House-Farm",
"Grete's House-Shop", "Marketplace-Farm",
"Marketplace-Post Office", "Marketplace-Shop",
"Marketplace-Town Hall", "Shop-Town Hall"
];
export const roadGraph = buildGraph(roads.map(r => r.split("-")));Dependency Sirkular
Dependency sirkular adalah situasi di mana modul A bergantung pada B, dan B juga, langsung atau gak langsung, bergantung pada A. Banyak sistem modul cuma ngelarang ini karena urutan apa pun yang kamu pilih buat ngeload modul kaya gitu, kamu gak bisa mastiin dependency setiap modul udah di-load sebelum dia jalan.
Modul CommonJS ngebolehin bentuk terbatas dari dependency siklik. Selama modul-modulnya gak ngakses interface satu sama lain sampai setelah mereka selesai di-load, dependency siklik gak apa-apa.
Fungsi require yang dikasih sebelumnya di bab ini mendukung jenis siklus dependency ini. Bisa kamu lihat gimana dia nanganin siklus?
Petunjuk:
Triknya adalah require nambahin objek interface buat sebuah modul ke cache-nya sebelum dia mulai ngeload modulnya. Dengan cara itu, kalau pemanggilan require mana pun yang dibikin waktu dia jalan nyoba ngeloadnya, dia udah dikenal, dan interface saat ini bakal dikembaliin, bukannya mulai ngeload modulnya sekali lagi (yang akhirnya bakal bikin stack overflow).