Prinsip SOLID yang paling sering disalahpahami adalah huruf D: Dependency Inversion Principle (DIP). Banyak orang mengiranya sekadar “pakai interface di mana-mana”, padahal intinya adalah arah panah dependensi. Tanpa DIP, modul domain — kode yang mengurus aturan bisnis — justru bergantung pada modul teknis seperti pengirim email atau driver database. Akibatnya, keputusan teknis kecil bisa memaksa kode bisnis ikut berubah. Chapter ini membedah cara membalik panah tersebut, ke mana abstraksi ditempatkan, dan bagaimana pola ini menjadi fondasi arsitektur hexagonal dan clean architecture.
Masalah Panah yang Menunjuk Detail #
Perhatikan kode domain yang lazim kita temui pada aplikasi awal:
// Domain langsung memanggil infrastruktur konkret
class LayananPesanan {
private final PengirimSms pengirim = new PengirimSms();
void antar(String pelanggan, String isi) {
System.out.println("Layanan memproses pesanan untuk " + pelanggan);
pengirim.kirim(pelanggan, isi);
}
}Kode tersebut sah dan berjalan, tetapi arah panahnya bermasalah: LayananPesanan mengimpor dan menginstansiasi PengirimSms. Ketika pihak bisnis meminta kanal email, kita harus membuka class domain, mengubah tipe field, lalu menguji ulang alur bisnisnya. Kode bisnis menjadi tahanan keputusan teknis.
Membalik Arah dengan Abstraksi #
DIP menyatakan dua aturan. Pertama, modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah — keduanya bergantung pada abstraksi. Kedua, abstraksi tidak boleh bergantung pada detail — detail yang bergantung pada abstraksi. Penerapannya menuntut satu keputusan penting: interface dideklarasikan di sisi domain, lalu infrastruktur mengimplementasikannya. Domain menyatakan “saya butuh sesuatu yang bisa mengirim pesan”; kanal SMS dan email menyesuaikan diri.
Perhatikan bahwa kedua panah implementasi kini mengalir dari infrastruktur menuju domain. Inilah makna kata “inversi”: bukan interface yang dibuat oleh pihak teknis untuk domain, melainkan sebaliknya. Program berikut memperlihatkan struktur lengkapnya:
public class PesanInversi {
public static void main(String[] args) {
LayananPesanan viaSms = new LayananPesanan(new PengirimSms());
LayananPesanan viaEmail = new LayananPesanan(new PengirimEmail());
viaSms.antar("Budi", "Pesanan #123 sudah dikirim");
viaEmail.antar("Siti", "Pesanan #124 sedang diproses");
}
}
// Abstraksi milik domain, bukan milik infrastruktur
interface PengirimPesan {
void kirim(String penerima, String isi);
}
// Modul tingkat tinggi: hanya mengenal interface
class LayananPesanan {
private final PengirimPesan pengirim;
LayananPesanan(PengirimPesan pengirim) {
this.pengirim = pengirim;
}
void antar(String pelanggan, String isi) {
System.out.println("Layanan memproses pesanan untuk " + pelanggan);
pengirim.kirim(pelanggan, isi);
}
}
// Infrastruktur A: kanal SMS
class PengirimSms implements PengirimPesan {
public void kirim(String penerima, String isi) {
System.out.println("[SMS] Kepada " + penerima + ": " + isi);
}
}
// Infrastruktur B: kanal email
class PengirimEmail implements PengirimPesan {
public void kirim(String penerima, String isi) {
System.out.println("[Email] Kepada " + penerima + "@example.com: " + isi);
}
}Output yang Dihasilkan:
Layanan memproses pesanan untuk Budi
[SMS] Kepada Budi: Pesanan #123 sudah dikirim
Layanan memproses pesanan untuk Siti
[Email] Kepada [email protected]: Pesanan #124 sedang diprosesMari kita bedah arah dependensi pada program tersebut:
- Baris 12-14 (
interface PengirimPesan): deklarasi interface berada di sampingLayananPesanan— satu paket domain. Isinya pun berbahasa domain:kirim(penerima, isi), bukan istilah teknis seperti nomor port atau protokol SMTP. - Baris 18 (
private final PengirimPesan pengirim): field domain bertipe interface. Compilerjavacmenolak bila kita menulisPengirimSmsdi sini karena classLayananPesanantidak mengimpor class infrastruktur apa pun. - Baris 3-4 (
new LayananPesanan(new PengirimSms())): pemilihan kanal terjadi dimain— satu-satunya tempat yang boleh mengenal semuanya. Struktur ini disebut composition root: titik perakitan di tepi aplikasi. - Baris 31-42 (
PengirimSms,PengirimEmail): kedua class infrastruktur menyesuaikan kontrak domain denganimplements PengirimPesan. Menambah kanal ketiga — misalnya pengirim log untuk pengujian — tidak menyentuh satu baris kode domain pun.
Kepemilikan Abstraksi Menentukan Segalanya #
Banyak tim menerapkan interface tetapi menempatkannya di paket infrastruktur — misalnya infra.PengirimPesan — lalu domain mengimpor paket tersebut. Secara sintaks tampak terbalik, tetapi arah panah paket tetap salah: domain bergantung pada infrastruktur. Uji tegasnya: hapus seluruh paket infrastruktur; kode domain harus tetap terkompilasi. Bila gagal, panah belum benar-benar dibalik.
DIP memberi struktur panah, tetapi objek infrastruktur tetap harus masuk ke dalam domain lewat pintu yang rapi. Mekanisme penyambungan itu — dependency injection — adalah topik chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
PesanInversi.java
Unduh berkaspublic class PesanInversi {
public static void main(String[] args) {
LayananPesanan viaSms = new LayananPesanan(new PengirimSms());
LayananPesanan viaEmail = new LayananPesanan(new PengirimEmail());
viaSms.antar("Budi", "Pesanan #123 sudah dikirim");
viaEmail.antar("Siti", "Pesanan #124 sedang diproses");
}
}
// Abstraksi milik domain, bukan milik infrastruktur
interface PengirimPesan {
void kirim(String penerima, String isi);
}
// Modul tingkat tinggi: hanya mengenal interface
class LayananPesanan {
private final PengirimPesan pengirim;
LayananPesanan(PengirimPesan pengirim) {
this.pengirim = pengirim;
}
void antar(String pelanggan, String isi) {
System.out.println("Layanan memproses pesanan untuk " + pelanggan);
pengirim.kirim(pelanggan, isi);
}
}
// Infrastruktur A: kanal SMS
class PengirimSms implements PengirimPesan {
public void kirim(String penerima, String isi) {
System.out.println("[SMS] Kepada " + penerima + ": " + isi);
}
}
// Infrastruktur B: kanal email
class PengirimEmail implements PengirimPesan {
public void kirim(String penerima, String isi) {
System.out.println("[Email] Kepada " + penerima + "@example.com: " + isi);
}
}