Chapter sebelumnya menutup dengan satu pertanyaan: bagaimana service mendapatkan repository yang tepat? Jawaban paling mudah — mengimpor satu repository global dari module — justru menutup pintu pertukaran yang telah kita bangun. Dependency injection (DI) menawarkan kebalikannya: kode tidak mencari atau membuat dependensinya sendiri, melainkan menerima dependensi tersebut dari luar. Pada chapter ini kita bandingkan kedua gaya tersebut lewat demo yang bisa dijalankan, lalu terapkan pola buatServer({ repo }) pada server HTTP sungguhan.
Masalah: Dependensi yang Ditanam Sendiri #
Mari kita mulai dari bentuk bermasalah. Berkas berikut menanam penyimpanan langsung di level module, lalu mendefinisikan fungsi yang memakainya:
// Penyimpanan ditanam langsung di level module:
// SEMUA pemanggil fungsi di bawah berbagi state yang sama.
const tugas = new Map();
let idBerikutnya = 1;
function buatTugas(judul) {
const lengkap = { id: idBerikutnya++, judul, status: 'belum' };
tugas.set(lengkap.id, lengkap);
return lengkap;
}
function hitungTugas() { return tugas.size; }
// Uji 1: berharap memulai dari penyimpanan kosong
console.log('uji 1:', JSON.stringify(buatTugas('Belajar DI')), '| total:', hitungTugas());
// Uji 2: berharap memulai dari kosong lagi - tetapi tidak bisa
console.log('uji 2:', JSON.stringify(buatTugas('Menulis laporan')), '| total:', hitungTugas());Output yang Dihasilkan:
uji 1: {"id":1,"judul":"Belajar DI","status":"belum"} | total: 1
uji 2: {"id":2,"judul":"Menulis laporan","status":"belum"} | total: 2Baris 3-4 menciptakan penyimpanan tepat satu kali saat module dievaluasi; fungsi buatTugas dan hitungTugas pada baris 6-12 menutup (close over) penyimpanan tersebut. Akibatnya terlihat di output: uji 2 berharap memulai dari keadaan kosong, tetapi totalnya 2 karena data uji 1 masih tersimpan. Dalam proyek nyata, gejala yang sama muncul sebagai pengujian yang saling bergantung urutan — dan sebagai ketidakmungkinan menukar Map dengan SQLite tanpa menyunting module ini beserta seluruh pemanggilnya.
Mekanisme: Terima dari Luar #
Dependency injection membalik arah penciptaan. Alih-alih fungsi mengambil dependensi dari scope module, dependensi dikirim masuk lewat parameter. Dalam JavaScript ada dua gaya yang paling umum:
| Gaya | Bentuk | Cocok untuk |
|---|---|---|
| Parameter per panggilan | simpan(repo, data) | Fungsi kecil, dependensi sedikit |
| Factory function | buatLayanan({ repo }) | Object dengan banyak method yang berbagi dependensi |
Gaya factory adalah pilihan utama aplikasi berlapis kita: satu panggilan buatLayananTugas({ repo }) menghasilkan object layanan lengkap yang seluruh method-nya menutup dependensi yang sama. Tiap panggilan factory menghasilkan instance baru dengan state baru — persis yang dibutuhkan untuk pengujian yang saling bebas.
Demo: Instance Independen dan Repo Palsu #
Berkas berikut menuliskan ulang layanan tugas dengan gaya factory. Perhatikan bahwa isi aturan bisnisnya tidak berubah dari chapter sebelumnya — yang berubah hanyalah cara dependensi masuk:
function buatRepoMemori() {
const tugas = new Map();
let idBerikutnya = 1;
return {
simpan(baru) {
const lengkap = { id: idBerikutnya++, ...baru };
tugas.set(lengkap.id, lengkap);
return lengkap;
},
semua() { return [...tugas.values()]; },
};
}
function buatLayananTugas({ repo }) {
return {
buat(judul) {
if (typeof judul !== 'string' || judul.trim() === '') {
throw new Error('JUDUL_WAJIB');
}
return repo.simpan({ judul: judul.trim(), status: 'belum' });
},
daftar() { return repo.semua(); },
};
}
// Dua layanan independen - masing-masing dengan repo sendiri
const layananA = buatLayananTugas({ repo: buatRepoMemori() });
const layananB = buatLayananTugas({ repo: buatRepoMemori() });
layananA.buat('Belajar DI');
layananB.buat('Rapat tim');
layananB.buat('Review kode');
console.log('layananA total:', layananA.daftar().length);
console.log('layananB total:', layananB.daftar().length);
// Repo palsu untuk pengujian: merekam panggilan tanpa menyimpan apa pun
function buatRepoPalsu() {
const panggilan = [];
return {
simpan(baru) { panggilan.push(baru); return { id: 1, ...baru }; },
semua() { return panggilan; },
};
}
const repoPalsu = buatRepoPalsu();
const layananUji = buatLayananTugas({ repo: repoPalsu });
console.log('valid :', JSON.stringify(layananUji.buat('Uji validasi')));
try { layananUji.buat(' '); } catch (e) { console.log('invalid :', e.message); }
console.log('direkam :', JSON.stringify(repoPalsu.semua()));Output yang Dihasilkan:
layananA total: 1
layananB total: 2
valid : {"id":1,"judul":"Uji validasi","status":"belum"}
invalid : JUDUL_WAJIB
direkam : [{"judul":"Uji validasi","status":"belum"}]Mari kita bedah hasilnya:
- Baris 1-12 (
buatRepoMemori): Kini setiap panggilan factory menciptakanMapdan penghitung id yang baru. State hidup di dalam closure hasil factory, bukan di scope module — dua panggilan menghasilkan dua penyimpanan yang sepenuhnya bebas. - Baris 14-24 (
buatLayananTugas): Factory layanan menerima{ repo }lewat destructuring parameter. Seluruh method hasil (buat,daftar) menutuprepoyang sama, sehingga satu layanan konsisten memakai satu penyimpanan. - Baris 27-33 (dua instance):
layananAdanlayananBdibangun dari kode factory yang persis sama, tetapi output membuktikan keduanya bebas — total 1 dan 2. Masalah “uji 2 melihat sisa uji 1” pada berkas sebelumnya tidak lagi ada. - Baris 36-42 (
buatRepoPalsu): Repo palsu (fake) memenuhi kontraksimpan/semuadengan merekam setiap panggilan ke array alih-alih menyimpan. Tidak ada aturan yang menyatakan repo harus sungguhan — yang penting kontraknya ditepati. - Baris 44-48 (menguji dengan repo palsu): Layanan uji dibangun di atas repo palsu. Baris valid membuktikan jalur bahagia tetap bekerja; baris
invalidmenangkap errorJUDUL_WAJIBdari aturan bisnis;direkammenunjukkan repo palsu hanya menerima panggilan yang lolos validasi. Inilah inti pengujian unit dengan dependensi terkontrol — dibahas penuh di chapter Mocking beserta panduan mocking Vitest .
Demo: buatServer({ repo }) pada HTTP Nyata #
Pola yang sama bekerja untuk server HTTP. Alih-alih membuat server yang membaca penyimpanan global, kita rakit server lewat factory yang menerima repository. Demo berikut menjalankan dua server dalam satu proses, masing-masing dengan data berbeda:
import http from 'node:http';
const host = '127.0.0.1';
function buatRepoMemori(awal) {
const tugas = new Map(awal.map((t) => [t.id, t]));
return { semua: () => [...tugas.values()] };
}
function buatServer({ repo }) {
return http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(repo.semua()));
});
}
async function ujiServer(repo, label) {
const server = buatServer({ repo });
await new Promise((selesai) => server.listen(0, host, selesai));
const port = server.address().port;
const r = await fetch(`http://${host}:${port}/tugas`);
console.log(label, '->', r.status, await r.text());
server.close();
server.closeAllConnections();
}
await ujiServer(buatRepoMemori([{ id: 1, judul: 'Belajar DI' }]), 'server A');
await ujiServer(buatRepoMemori([{ id: 1, judul: 'Rapat tim' }, { id: 2, judul: 'Review kode' }]), 'server B');Output yang Dihasilkan:
server A -> 200 [{"id":1,"judul":"Belajar DI"}]
server B -> 200 [{"id":1,"judul":"Rapat tim"},{"id":2,"judul":"Review kode"}]Bedah singkatnya:
- Baris 4-7: Repo versi ringkas menerima data awal sebagai parameter — trik yang membuat setiap server bisa disetel dengan dataset berbeda tanpa menulis ulang kode.
- Baris 9-14 (
buatServer): Factory ini memakaihttp.createServerdan menutuprepodi dalam callback handler. Satu definisi server, berbagai kemungkinan isi. - Baris 16-24 (
ujiServer): Helper uji memakai pola deterministik dari Section REST API:listen(0)di127.0.0.1untuk port sementara, satufetch, laluclose()dancloseAllConnections()sebelum melanjutkan. Hanya status dan body yang dicetak. - Baris 26-27: Dua server dijalankan berurutan dengan repository berbeda. Output menunjukkan
server Adanserver Bmenyajikan data masing-masing — kode server yang sama, dependensi yang berbeda, tanpa saling mengganggu.
Seberapa Jauh Kita Menyuntik #
Pola ini bisa didorong sampai jauh — sampai titik di mana kerangka kerja ikut mengatur seluruh suntikan. Sebelum tergoda, mari kita lihat batas praktisnya:
Aturan praktisnya: suntikkan dependensi yang bisa berbeda antar lingkungan atau antar pengujian — repository, pengirim email, jam, generator id. Biarkan tetap internal hal-hal yang tidak pernah berubah, misalnya Map sementara di dalam satu fungsi. Kita sudah melihat contoh jam yang disuntik pada Section Security (rate limiter dengan jam palsu) dan akan melihatnya lagi pada chapter Event-Driven Architecture.
Dengan repository di tangan dan cara menyuntikkannya jelas, satu komponen terakhir dari aplikasi berlapis masih berupa bayangan: service yang berdiri di tengah. Chapter Pola Service Layer membesarkan peran tersebut — rute yang tipis, aturan bisnis yang terpusat, dan pemetaan error domain ke status code.