Dependency Injection — Design Patterns

Dependency Injection (DI) adalah teknik di mana object menerima dependency-nya dari luar, bukan membuat sendiri. Variant konkret dari prinsip Inversion of…

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

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)
  }
}

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")

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

Latihan: UserService dengan constructor injection

Latihan nanti memakai versi class:

Starter mendaftarkan Budi dan Ani, jadi console-nya Budi, Ani, Unknown (untuk id 999), lalu 2 (isi repo.users).

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari