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 Contr

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

Kenapa penting:

Object yang membuat sendiri dependency-nya adalah sumber banyak derita:

// BAD — tight coupling
class UserService {
  constructor() {
    this.db = new PostgresConnection("prod-db.example.com");
    this.mailer = new SendGridMailer(process.env.SENDGRID_KEY);
    this.logger = new CloudwatchLogger();
  }
  async register(email) {
    // ... pakai this.db, this.mailer, this.logger
  }
}

Masalah: tidak bisa di-unit-test (selalu hit Postgres real), tidak bisa swap driver tanpa ubah class, tidak bisa pakai fake logger di dev. Object ini tergantung ke implementasi konkret.

// GOOD — dependency injected dari luar
class UserService {
  constructor(db, mailer, logger) {
    this.db = db;
    this.mailer = mailer;
    this.logger = logger;
  }
  async register(email) { /* ... */ }
}

// Production:
const service = new UserService(
  new PostgresConnection("prod-db.example.com"),
  new SendGridMailer(key),
  new CloudwatchLogger(),
);

// Test:
const service = new UserService(
  new InMemoryDb(),
  new FakeMailer(),
  new SilentLogger(),
);

Service sekarang tidak tahu (dan tidak peduli) implementasi konkret — dia hanya butuh object yang memenuhi kontrak. Testability meroket.

Tiga jenis injeksi:

1. Constructor injection (paling umum, direkomendasikan):

class OrderService {
  constructor(repo, emailer, clock) {
    this.repo = repo;
    this.emailer = emailer;
    this.clock = clock;
  }
}

Semua dependency wajib terpenuhi saat object dibuat. Tidak ada "partially constructed" state.

2. Setter injection (untuk dependency optional):

class ReportGenerator {
  setFormatter(formatter) { this.formatter = formatter; }
}

3. Interface / method injection (disuntik per-call):

class PaymentProcessor {
  process(order, gateway) { return gateway.charge(order); }
}

DI Container (service container):

Di aplikasi besar, wiring manual (new A(new B(new C()))) jadi ribet. DI Container otomatis resolve graph dependency:

// Pseudo — mirip Laravel / NestJS
container.bind("logger", () => new CloudwatchLogger());
container.bind("db", () => new PostgresConnection(...));
container.bind("userService", c =>
  new UserService(c.get("db"), c.get("mailer"), c.get("logger"))
);

const service = container.get("userService");
// Container resolve chain otomatis

Container tahu cara instantiate graph dan lifecycle (singleton, scoped, transient).

Dunia nyata di ekosistem JS/Laravel:

DI vs Service Locator — beda yang sering bikin bingung:

DI lebih baik karena dependency explicit di signature — gampang lihat apa yang dibutuhkan, gampang di-mock di test. Service Locator anti-pattern kebanyakan pengecualian karena "hidden dependency".

DI vs Hexagonal Architecture:

DI adalah teknik teknis. Hexagonal adalah arsitektur. Hexagonal perlu DI (untuk inject adapter ke domain), tapi DI bisa dipakai tanpa ikut gaya Hexagonal. DI = tool. Hexagonal = cara susun sistem pakai tool itu.

Kapan pakai:

Kapan JANGAN over-do:

Trade-off:

DI adalah investasi awal kecil (tulis constructor accept param, setup wiring) untuk payoff besar dalam testability dan flexibility. Overhead mental awal kecil dibanding utang teknis dari code yang tidak testable. Mulai sederhana (constructor injection manual), baru pakai container saat wiring manual mulai ribet.

Yang akan kamu pelajari