Dependency Injection (DI) berarti sebuah function atau object menerima dependency-nya dari luar, bukan membuat atau mengambilnya sendiri. Dependency adalah object lain yang ia butuhkan untuk bekerja: penyimpanan, pengirim pesan, counter nomor resi. Bayangin koki di dapur restoran. Bahan-bahannya disiapkan dan diantar ke mejanya; dia gak pergi ke pasar sendiri di tengah memasak. Saat koki baru diuji, yang diantar bahan latihan, dan resepnya sama persis. Koki yang belanja sendiri tiap ada pesanan gak bisa diuji tanpa pasar.
🧪 Tebakan cepat: di versi service locator (dibahas di bawah), test pemesanan Kurir Kancil lupa mendaftarkan fake notifier, lalu memesan dua paket. Test-nya lulus atau gagal, dan berapa SMS sungguhan yang terkirim? Tebak dulu, jawabannya ada di bagian "Service locator: dependency yang disembunyikan".
Masalahnya: createBooking yang mengambil sendiri
Versi lama core pemesanan Kurir Kancil mengambil semua kebutuhannya sendiri, termasuk ResiCounter dari lesson Singleton:
function createBooking() {
return {
pesan({ penerima, hp, service, kg }) {
if (!Object.hasOwn(TARIF, service)) throw new Error("Unknown service: " + service)
const resi = ResiCounter.getInstance().next() // singleton
const shipment = { resi, penerima, hp, service, kg, ongkir: kg * TARIF[service], status: "dijemput" }
shipmentDb.set(resi, shipment) // Map global
smsAdapter.kirim(hp, resi + " dijemput, ongkir Rp " + shipment.ongkir) // adapter produksi
return shipment
},
}
}
Test-nya mencoba memberi penyimpanan memori dan fake notifier, createBooking({ store: createInMemoryStore(), notifier: fake }), tapi JavaScript diam-diam mengabaikan argumen yang gak diminta. Dua test, masing-masing memesan satu paket dan mengharapkan KCL-0001:
SMS gateway → 081234567890: KCL-0001 dijemput, ongkir Rp 36000
gagal: kilat 2 kg: KCL-0001 Rp 36000, pesan tercatat 0
SMS gateway → 085711112222: KCL-0002 dijemput, ongkir Rp 30000
gagal: reguler 3 kg: KCL-0001 Rp 30000, dapat KCL-0002 Rp 30000
Dua SMS sungguhan terkirim dari sebuah test, fake-nya mencatat 0 pesan, dan test kedua dapat KCL-0002 karena singleton-nya membawa hitungan dari test pertama. Dari barisnya saja, createBooking() gak bilang ia butuh counter, database, dan vendor SMS.
DI: dependency dititipkan dari luar
Kamu sudah menulis DI di lesson Ports & Adapters, cuma belum menyebut namanya:
function createBooking({ store, notifier }) {
return {
pesan({ penerima, hp, service, kg }) {
if (!Object.hasOwn(TARIF, service)) throw new Error("Unknown service: " + service)
const resi = store.nextResi()
const shipment = { resi, penerima, hp, service, kg, ongkir: kg * TARIF[service], status: "dijemput" }
store.save(shipment)
notifier.kirim(hp, resi + " dijemput, ongkir Rp " + shipment.ongkir)
return shipment
},
}
}
// main.js: satu-satunya tempat merakit
const booking = createBooking({ store: createApiStore(api), notifier: smsAdapter })
// booking.test.js: test merakit versinya sendiri
function siapkan() {
const terkirim = []
const booking = createBooking({
store: createInMemoryStore(), // counter resi baru tiap test
notifier: { kirim: (hp, pesan) => terkirim.push(pesan) }, // fake: cuma mencatat
})
return { booking, terkirim }
}
// lulus: kilat 2 kg: KCL-0001 Rp 36000
// lulus: reguler 3 kg: KCL-0001 Rp 30000
createBooking({ store, notifier })menulis kebutuhannya di parameter. Siapa pun yang membaca baris itu tahu apa yang harus disiapkan.main.jsadalah composition root: satu tempat yang memilih dependency produksi dan merakitnya.- Test merakit versinya sendiri dengan penyimpanan memori dan fake: 2 dari 2 lulus, 0 SMS sungguhan.
- Singleton dari lesson Singleton, dipikir ulang. Aplikasi tetap bisa punya tepat satu store:
main.jsmembuatnya sekali dan menitipkannya ke semua yang butuh. Bedanya, "cuma satu" sekarang diputuskan dimain.js, bukan dikunci di dalam class lewatgetInstance(), jadi tiap test bebas membuat store baru yang mulai dariKCL-0001.
Nama Dependency Injection berasal dari artikel Martin Fowler tahun 2004, "Inversion of Control Containers and the Dependency Injection pattern". Inversion of Control artinya kendalinya dibalik: bukan createBooking yang mencari dependency, tapi pemanggilnya yang memberikan.
Tiga cara menitipkan
class BookingService {
constructor(store, notifier) { // 1. constructor injection: wajib, dititipkan saat dibuat
this.store = store
this.notifier = notifier
}
setLogger(logger) { // 2. setter injection: opsional, dipasang sesudahnya
this.logger = logger
}
cetakLabel(shipment, printer) { // 3. method injection: berganti tiap panggilan
printer.cetak(shipment.resi + " " + shipment.penerima)
}
}
- Constructor injection untuk dependency wajib: object-nya gak pernah ada dalam keadaan setengah jadi.
createBooking({ store, notifier })adalah versi function-nya,UserServicedi latihan versi class-nya. (Field injection di framework seperti Spring mengisi properti tanpa lewat constructor, jadi kebutuhannya gak kelihatan; untuk dependency wajib tetap pilih constructor.) - Setter injection untuk dependency opsional, misalnya logger. Di antara
newdansetLogger, object-nya belum lengkap. - Method injection untuk dependency yang berganti tiap panggilan, misalnya printer yang beda per gudang.
Container: merakit otomatis, tetap di main.js
Saat dependency-nya puluhan dan saling bergantung, merakit dengan tangan di main.js jadi panjang. DI container menyimpan resep cara membuat tiap dependency, lalu merakitnya saat diminta:
function createContainer() {
const factories = new Map()
const instances = new Map()
return {
register(name, factory) { factories.set(name, factory) },
resolve(name) {
if (!instances.has(name)) instances.set(name, factories.get(name)(this))
return instances.get(name)
},
}
}
// main.js
const container = createContainer()
container.register("store", () => createApiStore(api))
container.register("notifier", () => smsAdapter)
container.register("booking", (c) => createBooking({ store: c.resolve("store"), notifier: c.resolve("notifier") }))
const booking = container.resolve("booking")
registermenyimpan resep.resolvemenjalankan resepnya sekali lalu menyimpan hasilnya diinstances, jadistoredibuat sekali per container: singleton yang dikelola container, bukan oleh class-nya sendiri.createBookinggak berubah satu baris pun, dan test-nya tetap merakit langsung, tanpa container. Untuk dua dependency, container ini lebih panjang dari satu baris rakitan tangan: mulai dengan argumen biasa, pakai container saat rakitannya banyak.
Service locator: dependency yang disembunyikan
Service locator juga punya register dan get, tapi dipanggil dari dalam core:
const locator = {
services: new Map(),
register(name, service) {
this.services.set(name, service)
},
get(name) {
if (!this.services.has(name)) throw new Error("Service not registered: " + name)
return this.services.get(name)
},
}
// bootstrap.js: jalan saat aplikasi dimuat, ikut jalan saat test
locator.register("store", createApiStore(api))
locator.register("notifier", smsAdapter)
createBooking() kembali tanpa parameter, dan pesan mengambil sendiri const store = locator.get("store") dan const notifier = locator.get("notifier"); sisanya sama dengan versi DI. Test-nya mendaftarkan locator.register("store", createInMemoryStore()), tapi lupa notifier. Hasilnya: lulus dua kali, 2 dari 2, dan 2 SMS sungguhan terkirim (SMS gateway → 081234567890: KCL-0001 dijemput, ongkir Rp 36000, lalu satu lagi ke 085711112222). Itu jawaban tebakannya. Locator masih memegang smsAdapter dari bootstrap, dan dari createBooking() gak ada tanda ia butuh notifier. Di versi DI, lupa menitipkan notifier gak bisa diam-diam: test-nya berhenti dengan TypeError: Cannot read properties of undefined (reading 'kirim') dan 0 SMS terkirim.
Fowler (2004) membandingkan kedua cara ini; Mark Seemann (2010) menyebut service locator anti-pattern karena dependency-nya tersembunyi dari signature, jadi sulit dilihat dan sulit diganti fake di test. Pembedanya siapa yang memanggil: container yang dipanggil cuma di main.js adalah DI, container yang dioper ke dalam createBooking lalu di-resolve di sana adalah service locator lagi.
💡 Di framework: Laravel dan Angular
- Laravel membaca type-hint di constructor (
public function __construct(ShipmentStore $store)) dan membuat object-nya sendiri.$this->app->bind(ShipmentStore::class, ApiShipmentStore::class)memilih implementasinya,$this->app->singleton(...)membuatnya sekali, dan di test$this->app->instance(ShipmentStore::class, $fake)menukarnya dengan fake. - Angular punya injector bawaan: component meminta dependency lewat parameter constructor atau
inject(ShipmentStore), dan di testTestBedbisa memberi provider palsu.
Latihan: UserService dengan constructor injection
Latihan nanti memakai versi class:
createFakeRepo(): penyimpanan tiruan di memori. Kembalikan{ users, find, save }:usersadalah array yang bisa dibaca dari luar,find(id)mengembalikan user denganiditu ataunull, dansave(user)memberiidberurutan mulai dari 1, menyimpan{ id, name }, lalu mengembalikannya.class UserService:constructor(userRepository)cuma menyimpan repo yang dititipkan kethis.userRepository.getUserName(id)mengembalikannamedarithis.userRepository.find(id), atau"Unknown"kalau hasilnyanull.register(name)mengembalikan hasilthis.userRepository.save({ name }).
Starter mendaftarkan Budi dan Ani, jadi console-nya Budi, Ani, Unknown (untuk id 999), lalu 2 (isi repo.users).
⚠️ Jebakan yang sering ketemu
- Default yang menunjuk ke produksi.
function createBooking({ store, notifier = smsAdapter })terlihat praktis, tapi test yang lupa menitipkan notifier diam-diam mengirim SMS sungguhan, persis seperti service locator. - Container yang dioper ke dalam.
createBooking(container)lalucontainer.resolve("notifier")di dalamnya adalah service locator. Container cuma dipanggil dimain.js. - Parameter dependency yang terus memanjang. Function yang butuh tujuh dependency biasanya mengerjakan terlalu banyak. Pecah function-nya, jangan sembunyikan dependency-nya.
- Di latihan: membuat repo di dalam constructor.
this.userRepository = createFakeRepo()mengabaikan repo yang dititipkan, jadirepo.usersmilik test gak bertambah dan cekSari|1|UnknownjadiSari|0|Unknown. Id juga harus mulai dari 1, bukan 0.
TL;DR: Dependency Injection: function atau object menerima dependency-nya dari luar (createBooking({ store, notifier }), new UserService(repo)), bukan membuat atau mengambilnya sendiri. Kebutuhannya kelihatan di signature, main.js merakit versi produksi, dan test merakit versinya sendiri dengan penyimpanan memori dan fake: 2 dari 2 lulus, 0 SMS sungguhan. Constructor injection untuk dependency wajib, setter injection untuk yang opsional, method injection untuk yang berganti tiap panggilan. Singleton ResiCounter.getInstance() diganti store yang dibuat sekali di main.js, jadi tiap test bisa mulai dari KCL-0001. Container (Laravel, Angular, atau createContainer kecil) cuma mengotomatiskan rakitan dan tetap dipanggil di main.js. Service locator (locator.get("notifier") di dalam core) menyembunyikan dependency: test yang lupa mendaftarkan fake tetap lulus sambil mengirim 2 SMS sungguhan.