Setiap layanan aplikasi bergantung pada bagian lain: layanan pesanan bergantung pada pengirim email, modul laporan bergantung pada penyedia data, dan hampir semuanya bergantung pada penanda waktu. Cara paling cepat menuliskannya adalah membuat dependensi itu langsung di dalam layanan — pengirim email dibuat di baris kedua, dan selesai. Masalahnya muncul saat layanan itu mau diuji: setiap pemanggilan fungsi uji mengirim email sungguhan, dan kita tidak punya pintu untuk menggantinya dengan yang tiruan.
Dependency injection (injeksi dependensi) adalah kebiasaan yang membalik arah itu: dependensi tidak dibuat di dalam, melainkan diterima lewat parameter. Section Testing sudah menyentuh kebiasaan ini dari sisi alat uji; chapter ini membahasnya sebagai keputusan rancangan — di mana pintu itu diletakkan, siapa yang menyambungkannya, dan apa yang menjadi kontrak antar bagian.
Langkah pertama adalah mengenali mana yang layak disebut dependensi. Kaidah sederhananya: semua yang menyentuh dunia luar atau hasilnya tidak pasti. Pengirim email, fungsi fetch, penanda waktu (Date.now()), pembangkit angka acak, dan penyimpanan lokal semuanya masuk kategori ini — keluarannya tidak sepenuhnya kendali kode kita. Sebaliknya, function murni seperti penghitung total tidak perlu disuntik: ia tidak membawa kejutan, dan menyuntiknya hanya menambah kebisingan pada signature. Daftar dependensi yang disuntik, pada bagian yang tertata, cenderung pendek dan stabil.
Masalah: Dependensi yang Dibuat Sendiri #
Bentuk kode yang menutup pintu itu terlihat seperti ini:
function buatLayananPesanan() {
// Pengirim dibuat di dalam: tidak bisa diganti dari luar.
const pengirim = { kirim(penerima, pesan) { /* kirim email sungguhan */ } };
return {
proses(pesanan) {
pengirim.kirim(pesanan.email, `Pesanan #${pesanan.nomor} sedang diproses`);
return { ...pesanan, status: "diproses" };
},
};
}Kode ini sah dan berjalan, tetapi keputusannya rapat: pengirim dibuat di baris kedua dan tak ada jalur untuk menggantinya. Akibatnya tiga hal sekaligus terbata:
- Tidak bisa diuji — menguji
prosesselalu mengirim email; uji menjadi lambat, tidak pasti, dan berulang kali mengganggu data nyata. - Tidak bisa diganti — bila toko Budi berpindah dari email ke SMS, badan layanan harus disunting; satu-satunya yang berubah padahal hanya penyampai pesannya.
- Tersirat secara tersembunyi — signature
buatLayananPesanan()terlihat tanpa kebutuhan apa pun, padahal ia diam-diam membutuhkan akses keluar. Pemanggil tidak diberi tahu.
Prinsip: Terima, Jangan Buat Sendiri #
Kebalikannya berlaku dengan satu perubahan kecil: layanan menerima pengirimnya sebagai parameter. Berkas berikut memakai pola itu, lalu memakai layanan yang sama dua kali — sekali dengan pengirim asli, sekali dengan pengirim tiruan untuk pengujian:
// layanan-pesanan.js — dependensi diterima lewat parameter, bukan dibuat di dalam.
// Jalankan di terminal: node layanan-pesanan.js
// Dependensi asli: pengirim pemberitahuan (di sini disimulasikan dengan console).
const pengirimEmail = {
kirim(penerima, pesan) {
console.log(`[email] ke ${penerima}: ${pesan}`);
return "terkirim";
},
};
// Layanan: dependensinya diterima sebagai parameter saat layanan dibuat.
function buatLayananPesanan({ pengirim }) {
return {
proses(pesanan) {
const bukti = pengirim.kirim(
pesanan.email,
`Pesanan #${pesanan.nomor} sedang diproses`
);
return { ...pesanan, status: "diproses", bukti };
},
};
}
// ===== Pemakaian nyata: disambung dengan pengirim email =====
const layanan = buatLayananPesanan({ pengirim: pengirimEmail });
const hasil = layanan.proses({ nomor: 501, nama: "Budi", email: "[email protected]" });
console.log(hasil.status, hasil.bukti);
// ===== Pengganti untuk pengujian: bentuk sama, perilaku dicatat =====
function buatPengirimTiruan() {
const terkirim = [];
return {
terkirim,
kirim(penerima, pesan) {
terkirim.push({ penerima, pesan });
return "tiruan";
},
};
}
const pengirimTiruan = buatPengirimTiruan();
const layananUji = buatLayananPesanan({ pengirim: pengirimTiruan });
const hasilUji = layananUji.proses({ nomor: 502, nama: "Siti", email: "[email protected]" });
console.log(hasilUji.status, hasilUji.bukti);
console.log("Tercatat tiruan:", JSON.stringify(pengirimTiruan.terkirim[0]));Output yang Dihasilkan:
[email] ke [email protected]: Pesanan #501 sedang diproses
diproses terkirim
diproses tiruan
Tercatat tiruan: {"penerima":"[email protected]","pesan":"Pesanan #502 sedang diproses"}Mari kita telaah baris-baris pentingnya:
- Baris 13 (
function buatLayananPesanan({ pengirim }) {): kebutuhan layanan kini tertulis terbuka di parameternya — destructuring object membuat nama dependensinya terbaca seperti daftar kebutuhan. Signature ini adalah kontrak layanan: “aku butuh sesuatu yang punyakirim”. - Baris 16 (
const bukti = pengirim.kirim(...)): layanan hanya tahu bentuk kebutuhannya, bukan wujud aslinya. Ia memanggilkirimtanpa peduli apakah yang menjawab adalah email nyata, perekam tiruan, atau pengirim SMS minggu depan. - Baris 20 (
return { ...pesanan, status: "diproses", bukti };): hasil pengirim disertakan di object yang dikembalikan. Informasi ini penting untuk pengujian: pemanggil bisa memastikan pesanan benar-benar sampai. - Baris 26 (
const layanan = buatLayananPesanan({ pengirim: pengirimEmail });): penyambungan versi asli terjadi di luar layanan. Baris ini satu-satunya tempat yang mengetahui wujud pengirim sungguhan. - Baris 31–40 (
buatPengirimTiruan): pengganti uji punya bentuk yang persis sama — methodkirimdengan parameter dan nilai kembalian yang sejenis — tetapi perilakunya murni mencatat. Ia tidak menyentuh jaringan, sehingga uji bisa berjalan seribu kali tanpa efek samping. - Baris 43–46 (
const layananUji = ...): layanan yang sama persis, disambung dengan pengirim berbeda. Dua keluaran terakhir membuktikannya: statusnya sama, tetapi buktinyatiruandan isinya terekam diterkirim.
Ada dua gaya menyuntik, dan contoh di atas memakai yang pertama: injeksi saat dibuat — dependensi diterima sekali oleh factory, lalu semua pemanggilan proses memakainya. Gaya kedua, injeksi saat dipanggil, menerima dependensi di setiap pemanggilan: proses(pesanan, pengirim). Gaya kedua cocok untuk function murni yang dependensinya bisa berubah tiap panggilan — misalnya jam yang ditirukan berbeda-beda antar kasus uji. Bila dependensinya konstan selama layanan hidup — pengirim email untuk layanan pesanan biasanya iya — injeksi saat dibuat lebih hemat: penyambungan cukup sekali di composition root, dan method proses berfokus pada data pesanan saja.
Menyambungkan di Satu Tempat #
Injeksi menghadirkan pertanyaan baru: siapa yang memanggil buatLayananPesanan({ pengirim: ... }) dengan nilai asli? Jawabannya satu tempat saja — titik masuk aplikasi, yaitu berkas yang paling atas (di contoh kita, utama.mjs; di aplikasi browser, skrip utama yang memuat semuanya). Tempat penyambungan ini punya nama lazim: composition root.
Aturannya: bagian dalam aplikasi (layanan, module logika) menerima; hanya titik masuk yang menyambung dengan wujud asli. Bila wujud asli berubah — pengirim email diganti layanan pihak ketiga — yang disunting hanya satu baris di titik masuk, bukan seluruh layanan. Kontrasnya dengan pengirim tiruan: wujud tiruan itu hanya disambung di berkas uji, tidak pernah di titik masuk.
Bila layanan bertambah, composition root tumbuh dengan pola yang sama: satu daftar penyambungan yang terbaca dari atas ke bawah — pengirim email dibuat, layanan pesanan disambung, layanan laporan disambung. Berkas ini lambat berubah karena isinya hanya penyambungan; itulah tanda tempatnya benar. Kebalikannya menjadi tanda peringatan: bila modul logika mulai mengimpor library jaringan atau database secara langsung, penyambungan sedang menyusup ke tempat yang seharusnya murni.
Kontraknya pun sederhana untuk dijaga: dependensi tiruan harus punya method dengan nama dan parameter yang sama dengan aslinya. Section Testing sudah mempraktikkan pola ini dengan stub dan fake — t.mock.method milik node:test bisa menggantikan method pada object yang disuntik, sehingga uji bisa memastikan “email terkirim tepat sekali” tanpa email nyata. Bedanya kini jelas: t.mock adalah alat penggantinya, dan injeksi adalah pintunya — tanpa pintu, alat itu tak berdaya.
Dengan dependensi yang bisa diganti, satu jenis dependensi tersisa yang paling suka membuahkan kejutan: bagian yang gagal — jaringan, server, data yang tidak ada. Bagaimana aplikasi menata jalur kegagalannya secara teratur dari lapisan data sampai tampilan adalah topik chapter berikutnya, Error Handling Strategy.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
layanan-pesanan.js
Unduh berkas// layanan-pesanan.js — dependensi diterima lewat parameter, bukan dibuat di dalam.
// Jalankan di terminal: node layanan-pesanan.js
// Dependensi asli: pengirim pemberitahuan (di sini disimulasikan dengan console).
const pengirimEmail = {
kirim(penerima, pesan) {
console.log(`[email] ke ${penerima}: ${pesan}`);
return "terkirim";
},
};
// Layanan: dependensinya diterima sebagai parameter saat layanan dibuat.
function buatLayananPesanan({ pengirim }) {
return {
proses(pesanan) {
const bukti = pengirim.kirim(
pesanan.email,
`Pesanan #${pesanan.nomor} sedang diproses`
);
return { ...pesanan, status: "diproses", bukti };
},
};
}
// ===== Pemakaian nyata: disambung dengan pengirim email =====
const layanan = buatLayananPesanan({ pengirim: pengirimEmail });
const hasil = layanan.proses({ nomor: 501, nama: "Budi", email: "[email protected]" });
console.log(hasil.status, hasil.bukti);
// ===== Pengganti untuk pengujian: bentuk sama, perilaku dicatat =====
function buatPengirimTiruan() {
const terkirim = [];
return {
terkirim,
kirim(penerima, pesan) {
terkirim.push({ penerima, pesan });
return "tiruan";
},
};
}
const pengirimTiruan = buatPengirimTiruan();
const layananUji = buatLayananPesanan({ pengirim: pengirimTiruan });
const hasilUji = layananUji.proses({ nomor: 502, nama: "Siti", email: "[email protected]" });
console.log(hasilUji.status, hasilUji.bukti);
console.log("Tercatat tiruan:", JSON.stringify(pengirimTiruan.terkirim[0]));