Setelah arah dependensi dibalik pada chapter sebelumnya, satu pertanyaan tinggal: siapa yang memilih implementasi dan menyambungkannya ke dalam class domain? Dependency Injection (DI) adalah tekniknya — dependensi disuntikkan dari luar, bukan diciptakan oleh class itu sendiri. Class yang membangun dependensinya sendiri melalui new menjadi kaku: pilihan implementasi terkunci di dalam, sulit diganti, dan sulit diuji. Chapter ini membahas tiga gaya penyuntikan, bukti manfaat testabilitas dengan fake, serta cara kerja container DI dalam bentuk mini yang dapat dijalankan.
Masalah new yang Tersebar #
Perhatikan kembali pola bermasalah berikut:
// Class membangun dependensinya sendiri: kaku dan sulit diuji
class LayananPesanan {
private final PenyimpanMemori penyimpan = new PenyimpanMemori();
}LayananPesanan kini memiliki dua alasan berubah: aturan bisnis pesanan dan pilihan media penyimpanan. Saat pengujian berjalan, setiap pemanggilan method benar-benar menulis data — efek samping yang membuat pengujian lambat dan rapuh. Martin Fowler menyebut penyebab utamanya sebagai Dependency Injection yang belum diterapkan: objek mengambil dependensi alih-alih menerimanya.
Tiga Gaya Penyuntikan #
Java mengenal tiga titik suntik, masing-masing dengan kecocokan berbeda:
| Gaya | Mekanisme | Kapan dipakai |
|---|---|---|
| Constructor | lewat parameter constructor | dependensi wajib; pilihan utama |
| Setter | lewat method set... setelah objek lahir | dependensi opsional atau dapat diganti |
| Method | lewat parameter satu pemanggilan | dependensi hanya dibutuhkan method tersebut |
Program berikut memperagakan ketiganya sekaligus, lalu menutup dengan penyuntikan fake untuk keperluan pengujian:
import java.util.ArrayList;
import java.util.List;
public class LayananDenganInjeksi {
public static void main(String[] args) {
System.out.println("== Constructor injection ==");
LayananPesanan layanan = new LayananPesanan(new PenyimpanMemori());
layanan.buatPesanan("Budi", 3);
layanan.buatPesanan("Siti", 1);
System.out.println("== Method injection ==");
layanan.lapor(new PemformatBaris());
System.out.println("== Setter injection ==");
layanan.ubahPenyimpan(new PenyimpanLog());
layanan.buatPesanan("Dewi", 2);
System.out.println("== Fake untuk pengujian ==");
PenyimpanFake fake = new PenyimpanFake();
LayananPesanan yangDiuji = new LayananPesanan(fake);
yangDiuji.buatPesanan("Budi", 5);
System.out.println("Fake mencatat -> pelanggan: " + fake.terakhirPelanggan
+ ", jumlah: " + fake.terakhirJumlah);
}
}
interface PenyimpanPesanan {
long simpan(String pelanggan, int jumlah);
int jumlahPesanan();
}
class LayananPesanan {
private PenyimpanPesanan penyimpan;
LayananPesanan(PenyimpanPesanan penyimpan) {
this.penyimpan = penyimpan;
}
void ubahPenyimpan(PenyimpanPesanan penyimpanBaru) {
this.penyimpan = penyimpanBaru;
}
void buatPesanan(String pelanggan, int jumlah) {
long id = penyimpan.simpan(pelanggan, jumlah);
System.out.println("Pesanan #" + id + " untuk " + pelanggan);
}
void lapor(Pemformat format) {
System.out.println(format.tampilkan(penyimpan.jumlahPesanan()));
}
}
class PenyimpanMemori implements PenyimpanPesanan {
private final List<String> data = new ArrayList<>();
public long simpan(String pelanggan, int jumlah) {
data.add(pelanggan + " x" + jumlah);
return data.size();
}
public int jumlahPesanan() {
return data.size();
}
}
class PenyimpanLog implements PenyimpanPesanan {
private int hitung = 0;
public long simpan(String pelanggan, int jumlah) {
hitung++;
System.out.println("[Log] " + pelanggan + " memesan " + jumlah + " item");
return 900 + hitung;
}
public int jumlahPesanan() {
return hitung;
}
}
interface Pemformat {
String tampilkan(int jumlahPesanan);
}
class PemformatBaris implements Pemformat {
public String tampilkan(int jumlah) {
return "Total pesanan tersimpan: " + jumlah;
}
}
// Fake: merekam panggilan untuk pengujian, tanpa menyimpan apa pun
class PenyimpanFake implements PenyimpanPesanan {
String terakhirPelanggan;
int terakhirJumlah;
public long simpan(String pelanggan, int jumlah) {
terakhirPelanggan = pelanggan;
terakhirJumlah = jumlah;
return 1;
}
public int jumlahPesanan() {
return terakhirPelanggan == null ? 0 : 1;
}
}Output yang Dihasilkan:
== Constructor injection ==
Pesanan #1 untuk Budi
Pesanan #2 untuk Siti
== Method injection ==
Total pesanan tersimpan: 2
== Setter injection ==
[Log] Dewi memesan 2 item
Pesanan #901 untuk Dewi
== Fake untuk pengujian ==
Pesanan #1 untuk Budi
Fake mencatat -> pelanggan: Budi, jumlah: 5Mari kita bedah titik-titik suntik pada program tersebut:
- Baris 7 (
new LayananPesanan(new PenyimpanMemori())): constructor injection — implementasi masuk saat objek lahir, sehingga objek tidak pernah berada dalam keadaan setengah jadi. Fieldpenyimpansengaja tidakfinalkarena setter injection akan mengizinkan penggantian. - Baris 12 (
layanan.lapor(new PemformatBaris())): method injection —PemformatBarisdikirim sebagai argumen satu pemanggilan. Methodlaportidak menyimpan objek tersebut sebagai field; kebutuhannya benar-benar sesaat. - Baris 15 (
layanan.ubahPenyimpan(new PenyimpanLog())): setter injection mengganti penyimpan di tengah umur objek. Perhatikan hasil eksekusi: nomor pesanan melompat ke901karenaPenyimpanLogmemakai deret nomor berbeda — bukti bahwa penggantian implementasi tidak menyentuhLayananPesanan. - Baris 19-23 (
new PenyimpanFake()): fake tidak menyimpan apa pun; fake hanya merekamterakhirPelanggandanterakhirJumlah. Layanan yang sama berjalan tanpa perubahan, dan pengujian dapat memeriksa rekaman tersebut alih-alih membaca database sungguhan.
Container DI Mini #
Pada aplikasi nyata, penyambungan manual di main membengkak puluhan baris. Container DI menyelesaikannya dengan satu registry: kita mendaftarkan pabrik untuk setiap tipe, lalu container menyusun grafik objek secara otomatis. Versi mini berikut memakai Map<Class<?>, Supplier<?>>:
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class WadahSederhana {
public static void main(String[] args) {
Wadah wadah = new Wadah();
wadah.daftar(KatalogFilm.class, KatalogMemori::new);
wadah.daftar(LayananSewa.class,
() -> new LayananSewa(wadah.ambil(KatalogFilm.class)));
LayananSewa layanan = wadah.ambil(LayananSewa.class);
layanan.sewa("Budi", "Laskar Pelangi");
layanan.sewa("Siti", "Kelas 3 Cinta");
System.out.println("Total film tersewa: " + layanan.jumlahFilm());
}
}
// Container mini: registry tipe -> pabrik objek
class Wadah {
private final Map<Class<?>, Supplier<?>> pabrik = new HashMap<>();
<T> void daftar(Class<T> tipe, Supplier<T> pencipta) {
pabrik.put(tipe, pencipta);
}
@SuppressWarnings("unchecked")
<T> T ambil(Class<T> tipe) {
Supplier<?> pencipta = pabrik.get(tipe);
if (pencipta == null) {
throw new IllegalStateException("Tidak terdaftar: " + tipe.getSimpleName());
}
return (T) pencipta.get();
}
}
interface KatalogFilm {
void catat(String judul);
int jumlah();
}
class KatalogMemori implements KatalogFilm {
private final List<String> film = new ArrayList<>();
public void catat(String judul) {
film.add(judul);
System.out.println("[Katalog] " + judul + " tersewa");
}
public int jumlah() {
return film.size();
}
}
class LayananSewa {
private final KatalogFilm katalog;
LayananSewa(KatalogFilm katalog) {
this.katalog = katalog;
}
void sewa(String pelanggan, String judul) {
System.out.println(pelanggan + " menyewa " + judul);
katalog.catat(judul);
}
int jumlahFilm() {
return katalog.jumlah();
}
}Output yang Dihasilkan:
Budi menyewa Laskar Pelangi
[Katalog] Laskar Pelangi tersewa
Siti menyewa Kelas 3 Cinta
[Katalog] Kelas 3 Cinta tersewa
Total film tersewa: 2Tiga baris patut dicermati pada container tersebut:
- Baris 10-12 (
wadah.daftar(...)dua panggilan): setiap tipe didaftarkan bersama pabriknya —KatalogMemori::newuntuk interfaceKatalogFilm. Pendaftaran inilah satu-satunya tempat implementasi konkret disebut. - Baris 11-12 (lambda untuk
LayananSewa): pabrikLayananSewamemanggilwadah.ambil(KatalogFilm.class)sehingga dependensinya ikut ter-resolve secara rekursif. Pemanggil hanya memintaLayananSewatanpa tahu dependensinya. - Baris 22-37 (
class Wadah): registry berupaHashMap;ambilmenjalankanSupplierpada tiap permintaan. Container produksi seperti Spring menambahkan scope tunggal, pemindaian anotasi, dan siklus hidup objek — konsep dasarnya sama dengan 15 baris ini.
Injection membuat kolaborator datang dari luar, sehingga hubungan antarobjek menjadi keputusan perakitan, bukan keputusan yang terkunci di dalam class. Prinsip berikutnya menggeser fokus dari siapa menyambungkan ke bagaimana menyusun perilaku: composition over inheritance.
Referensi #
- Martin Fowler: Inversion of Control Containers and the Dependency Injection pattern
- JUnit 5 User Guide
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
LayananDenganInjeksi.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class LayananDenganInjeksi {
public static void main(String[] args) {
System.out.println("== Constructor injection ==");
LayananPesanan layanan = new LayananPesanan(new PenyimpanMemori());
layanan.buatPesanan("Budi", 3);
layanan.buatPesanan("Siti", 1);
System.out.println("== Method injection ==");
layanan.lapor(new PemformatBaris());
System.out.println("== Setter injection ==");
layanan.ubahPenyimpan(new PenyimpanLog());
layanan.buatPesanan("Dewi", 2);
System.out.println("== Fake untuk pengujian ==");
PenyimpanFake fake = new PenyimpanFake();
LayananPesanan yangDiuji = new LayananPesanan(fake);
yangDiuji.buatPesanan("Budi", 5);
System.out.println("Fake mencatat -> pelanggan: " + fake.terakhirPelanggan
+ ", jumlah: " + fake.terakhirJumlah);
}
}
interface PenyimpanPesanan {
long simpan(String pelanggan, int jumlah);
int jumlahPesanan();
}
class LayananPesanan {
private PenyimpanPesanan penyimpan;
LayananPesanan(PenyimpanPesanan penyimpan) {
this.penyimpan = penyimpan;
}
void ubahPenyimpan(PenyimpanPesanan penyimpanBaru) {
this.penyimpan = penyimpanBaru;
}
void buatPesanan(String pelanggan, int jumlah) {
long id = penyimpan.simpan(pelanggan, jumlah);
System.out.println("Pesanan #" + id + " untuk " + pelanggan);
}
void lapor(Pemformat format) {
System.out.println(format.tampilkan(penyimpan.jumlahPesanan()));
}
}
class PenyimpanMemori implements PenyimpanPesanan {
private final List<String> data = new ArrayList<>();
public long simpan(String pelanggan, int jumlah) {
data.add(pelanggan + " x" + jumlah);
return data.size();
}
public int jumlahPesanan() {
return data.size();
}
}
class PenyimpanLog implements PenyimpanPesanan {
private int hitung = 0;
public long simpan(String pelanggan, int jumlah) {
hitung++;
System.out.println("[Log] " + pelanggan + " memesan " + jumlah + " item");
return 900 + hitung;
}
public int jumlahPesanan() {
return hitung;
}
}
interface Pemformat {
String tampilkan(int jumlahPesanan);
}
class PemformatBaris implements Pemformat {
public String tampilkan(int jumlah) {
return "Total pesanan tersimpan: " + jumlah;
}
}
// Fake: merekam panggilan untuk pengujian, tanpa menyimpan apa pun
class PenyimpanFake implements PenyimpanPesanan {
String terakhirPelanggan;
int terakhirJumlah;
public long simpan(String pelanggan, int jumlah) {
terakhirPelanggan = pelanggan;
terakhirJumlah = jumlah;
return 1;
}
public int jumlahPesanan() {
return terakhirPelanggan == null ? 0 : 1;
}
}
WadahSederhana.java
Unduh berkasimport java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
public class WadahSederhana {
public static void main(String[] args) {
Wadah wadah = new Wadah();
wadah.daftar(KatalogFilm.class, KatalogMemori::new);
wadah.daftar(LayananSewa.class,
() -> new LayananSewa(wadah.ambil(KatalogFilm.class)));
LayananSewa layanan = wadah.ambil(LayananSewa.class);
layanan.sewa("Budi", "Laskar Pelangi");
layanan.sewa("Siti", "Kelas 3 Cinta");
System.out.println("Total film tersewa: " + layanan.jumlahFilm());
}
}
// Container mini: registry tipe -> pabrik objek
class Wadah {
private final Map<Class<?>, Supplier<?>> pabrik = new HashMap<>();
<T> void daftar(Class<T> tipe, Supplier<T> pencipta) {
pabrik.put(tipe, pencipta);
}
@SuppressWarnings("unchecked")
<T> T ambil(Class<T> tipe) {
Supplier<?> pencipta = pabrik.get(tipe);
if (pencipta == null) {
throw new IllegalStateException("Tidak terdaftar: " + tipe.getSimpleName());
}
return (T) pencipta.get();
}
}
interface KatalogFilm {
void catat(String judul);
int jumlah();
}
class KatalogMemori implements KatalogFilm {
private final List<String> film = new ArrayList<>();
public void catat(String judul) {
film.add(judul);
System.out.println("[Katalog] " + judul + " tersewa");
}
public int jumlah() {
return film.size();
}
}
class LayananSewa {
private final KatalogFilm katalog;
LayananSewa(KatalogFilm katalog) {
this.katalog = katalog;
}
void sewa(String pelanggan, String judul) {
System.out.println(pelanggan + " menyewa " + judul);
katalog.catat(judul);
}
int jumlahFilm() {
return katalog.jumlah();
}
}