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:
- Laravel Service Container —
app()->bind(UserRepository::class, EloquentUserRepository::class). Laravel autowire constructor pakai reflection: kamu type-hint di constructor, container tahu resolve. Saat test, swap binding. - Angular DI — provider system di modul/komponen. Decorator
@Injectable(),@Inject(). Token-based DI built into framework. - NestJS — Node.js framework dengan DI container ala Angular. Gunakan decorator TypeScript untuk declarative injection.
- InversifyJS / tsyringe — DI container library standalone untuk TypeScript/Node.js.
- React tidak punya DI built-in — tapi pattern mirip: Context untuk inject shared service, custom hooks untuk akses. Props itself adalah bentuk injection paling eksplisit.
DI vs Service Locator — beda yang sering bikin bingung:
- DI — dependency diberikan ke object via constructor/setter. Eksplisit di signature. Object pasif.
- Service Locator — object mengambil dependency dari registry global.
locator.get("db")di tengah method. Dependency tersembunyi dari signature.
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:
- Class punya dependency non-trivial (DB, HTTP, filesystem, clock)
- Butuh test unit tanpa infra real
- Ingin swap implementasi (prod vs dev vs test, atau ganti vendor)
- Team besar dengan banyak modul — DI menjaga boundary jelas
Kapan JANGAN over-do:
- Utility function murni (
formatDate) tidak butuh DI — langsung import - Library atau helper tipis — nambah DI container cuma kompleksitas
- Prototype / small script —
new X()langsung sudah ok, refactor kalau proyek tumbuh
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.