Tiga prinsip SOLID sebelumnya membentuk bahan bakunya: kontrak yang bersih, substitusi yang aman, dan interface yang terpisah per peran. Dependency Inversion Principle (DIP) — huruf “D” sekaligus penutup SOLID — adalah momen semua bahan itu dirakit menjadi arsitektur. Rumusannya punya dua bagian: modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah — keduanya harus bergantung pada abstraksi; dan abstraksi tidak boleh bergantung pada detail — detail yang harus bergantung pada abstraksi. Chapter ini membedah prinsipnya di level class dengan refactor penuh yang bisa dijalankan; sudut pandang arsitektur berlapis dibahas terpisah di chapter Dependency Inversion pada Arsitektur Aplikasi .

Kode yang Melanggar DIP #

Modul tingkat tinggi adalah kode yang berisi aturan bisnis — misalnya layanan yang memproses pesanan. Modul tingkat rendah adalah kode teknis yang bekerja untuk aturan itu — koneksi database, kirim email, akses file. Pelanggaran DIP terjadi ketika layanan bisnis membuat dan memegang detail teknis secara langsung:

php
<?php

class KoneksiSqlite
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = new PDO("sqlite::memory:");
        $this->pdo->exec(
            "CREATE TABLE pesanan (id INTEGER PRIMARY KEY, pembeli TEXT, total INTEGER)"
        );
    }

    public function simpan(string $pembeli, int $total): void
    {
        $statement = $this->pdo->prepare(
            "INSERT INTO pesanan (pembeli, total) VALUES (:pembeli, :total)"
        );
        $statement->execute(["pembeli" => $pembeli, "total" => $total]);
    }
}

class LayananPesanan
{
    public function buatPesanan(string $pembeli, int $total): void
    {
        $database = new KoneksiSqlite();
        $database->simpan($pembeli, $total);
        echo "Pesanan $pembeli Rp$total tersimpan\n";
    }
}

$layanan = new LayananPesanan();
$layanan->buatPesanan("Budi", 150000);

Output yang Dihasilkan:

text
Pesanan Budi Rp150000 tersimpan

Mari kita bedah letak masalahnya:

  1. Baris 3-22: KoneksiSqlite adalah modul tingkat rendah — ia tahu DSN, skema tabel, dan cara menulis ke SQLite. Semua wajar untuk class teknis.
  2. Baris 24-32: LayananPesanan adalah modul tingkat tinggi — aturan bisnisnya “pesanan harus tersimpan”. Namun di baris 28, di dalam buatPesanan(), layanan ini membuat KoneksiSqlite sendiri lewat new. Arah ketergantungan menunjuk ke bawah: bisnis bergantung pada detail teknis, dan itulah yang mau dibalik (di-invert) oleh DIP.
  3. Baris 34-35: pemanggil hanya bisa membuat layanan lalu memanggilnya; tidak ada jalan menyuntikkan koneksi lain, karena pilihan KoneksiSqlite sudah terkunci di dalam method.

Konsekuensi praktisnya nyata. Layanan ini tidak bisa diuji tanpa database sungguhan, karena koneksi dibuat di dalamnya dan tak tersumbat. Ketika aplikasi pindah dari SQLite ke PostgreSQL, LayananPesanan — kode bisnis yang seharusnya paling stabil — justru yang harus diedit. Perubahan di modul rendah memicu perubahan di modul tinggi, persis kebalikan dari yang seharusnya.

Refactor dengan Membalik Ketergantungan #

Langkah refactornya tegas: putus ketergantungan layanan ke class konkret dengan sebuah interface yang dimiliki sisi bisnis, lalu suntikkan implementasi dari luar lewat constructor:

php
<?php

interface PenyimpananPesanan
{
    public function simpan(string $pembeli, int $total): void;
}

final class PenyimpananSqlite implements PenyimpananPesanan
{
    public function __construct(private PDO $pdo)
    {
    }

    public function simpan(string $pembeli, int $total): void
    {
        $statement = $this->pdo->prepare(
            "INSERT INTO pesanan (pembeli, total) VALUES (:pembeli, :total)"
        );
        $statement->execute(["pembeli" => $pembeli, "total" => $total]);
    }
}

final class PenyimpananArray implements PenyimpananPesanan
{
    public array $pesanan = [];

    public function simpan(string $pembeli, int $total): void
    {
        $this->pesanan[] = ["pembeli" => $pembeli, "total" => $total];
    }
}

final class LayananPesanan
{
    public function __construct(private PenyimpananPesanan $penyimpanan)
    {
    }

    public function buatPesanan(string $pembeli, int $total): void
    {
        $this->penyimpanan->simpan($pembeli, $total);
        echo "Pesanan $pembeli Rp$total diproses\n";
    }
}

$pdo = new PDO("sqlite::memory:");
$pdo->exec("CREATE TABLE pesanan (id INTEGER PRIMARY KEY, pembeli TEXT, total INTEGER)");

$penyimpananNyata = new PenyimpananSqlite($pdo);
$layananProduksi = new LayananPesanan($penyimpananNyata);
$layananProduksi->buatPesanan("Budi", 150000);

$jumlah = (int) $pdo->query("SELECT COUNT(*) FROM pesanan")->fetchColumn();
echo "Baris di database: $jumlah\n";

$penyimpananUji = new PenyimpananArray();
$layananUji = new LayananPesanan($penyimpananUji);
$layananUji->buatPesanan("Siti", 275000);

print_r($penyimpananUji->pesanan);

Output yang Dihasilkan:

text
Pesanan Budi Rp150000 diproses
Baris di database: 1
Pesanan Siti Rp275000 diproses
Array
(
    [0] => Array
        (
            [pembeli] => Siti
            [total] => 275000
        )

)

Refactor ini memindahkan banyak hal sekaligus:

  1. Baris 3-6: PenyimpananPesanan kini milik sisi bisnis — kontraknya berbahasa bisnis (simpan pesanan), tidak ada jejak SQL. Detail-lah yang tunduk pada abstraksi ini, bukan sebaliknya.
  2. Baris 8-21: PenyimpananSqlite mengimplementasikan kontrak tersebut; seluruh detail PDO tetap hidup di sini.
  3. Baris 23-31: PenyimpananArray adalah implementasi kedua yang menyimpan ke array — bukti bahwa kontrak bisa dipenuhi detail lain tanpa menyentuh layanan.
  4. Baris 33-44: LayananPesanan menerima PenyimpananPesanan lewat constructor di baris 35. Tidak ada new untuk class teknis di dalam layanan lagi.
  5. Baris 46-51: kode eksternal menyusun objek — membuat PDO, membungkusnya ke PenyimpananSqlite, lalu menyuntikkannya ke layanan. Pergantian penyimpanan kini terjadi di luar layanan, sebagaimana ditunjukkan baris 56-58 yang memakai PenyimpananArray.

Arah panah ketergantungan sebelum dan sesudah refactor bisa diringkas begini:

Diagram tersebut memperlihatkan inti “inversi"nya: sebelumnya panah mengalir dari layanan turun ke KoneksiSqlite. Kini kedua detail justru mengarah ke abstraksi yang didefinisikan kebutuhan layanan. Modul tingkat rendah dan tingkat tinggi sama-sama bergantung pada kontrak — dan kontrak itu milik sisi bisnis.

Composition Root #

Kalau new sudah dilarang di dalam layanan, di mana objek dibuat? Jawabannya: di satu titik penyusunan bernama composition root — lokasi paling luar aplikasi, biasanya berkas bootstrap atau index.php, tempat seluruh object graph dirangkai sebelum aplikasi mulai bekerja. Contoh sederhananya adalah baris 46-51 pada kode di atas: satu blok yang tahu implementasi konkret mana yang dipakai, sementara sisa aplikasi hanya mengenal interface.

Composition root adalah satu-satunya tempat yang boleh menyebut nama class konkret, dan justru karena terkonsentrasi di satu titik, keputusan seperti “produksi memakai SQLite, pengembangan memakai array” menjadi satu baris yang mudah diubah. Pembahasan lebih lanjut tentang struktur berkas bootstrap dan penggantian implementasi lewat konfigurasi ada di chapter Application Bootstrap serta Dependency Injection .

Membedakan DI, DIP, dan DIC #

Tiga istilah ini sering dicampuradukkan padahal berdiri di kategori yang berbeda. Tabel berikut memisahkannya:

IstilahKategoriPertanyaan yang dijawab
Dependency Injection (DI)teknikbagaimana dependency diserahkan ke class — lewat constructor, setter, atau parameter
Dependency Inversion Principle (DIP)prinsip desainkepada siapa class boleh bergantung — abstraksi, bukan detail konkret
DI Container (DIC)alatsiapa yang menyusun dan meng-cache object graph — biasanya framework

Ketiganya bisa dipakai terpisah. Anda bisa menerapkan DI tanpa DIP — menyuntikkan class konkret PenyimpananSqlite langsung ke constructor tetaplah injection, tetapi layanan tetap terikat pada detail. Anda juga bisa menerapkan DIP tanpa DIC — seperti refactor kita di atas, yang seluruh penyusunannya ditulis tangan di composition root. Container (seperti yang disediakan framework) baru masuk ketika jumlah wiring manual mulai melelahkan; ia menjalankan DI sesuai konfigurasi, dan kualitas DIP-nya tetap ditentukan oleh desain interface Anda.

Imbalan bagi Pengujian #

Motivasi paling terasa dari DIP muncul saat menulis unit test. Karena LayananPesanan hanya mengenal kontrak PenyimpananPesanan, pengujian aturan bisnisnya tidak lagi memerlukan database sama sekali — cukup implementasi palsu yang cepat dan deterministik:

php
<?php

use PHPUnit\Framework\TestCase;

final class LayananPesananTest extends TestCase
{
    public function testBuatPesananMenyimpanData(): void
    {
        $penyimpanan = new PenyimpananArray();
        $layanan = new LayananPesanan($penyimpanan);

        $layanan->buatPesanan("Budi", 150000);

        $this->assertCount(1, $penyimpanan->pesanan);
        $this->assertSame("Budi", $penyimpanan->pesanan[0]["pembeli"]);
        $this->assertSame(150000, $penyimpanan->pesanan[0]["total"]);
    }
}

Test di atas memakai PenyimpananArray sebagai test double — objek pengganti yang merekam data alih-alih menulis ke database. Test berjalan dalam hitungan mikrodetik, tidak butuh server, dan hasilnya sama setiap dijalankan. Bandingkan dengan versi awal: LayananPesanan lama mustahil diuji tanpa database karena koneksi dibuat di dalam method-nya sendiri. Teknik pembuatan test double yang lebih canggih (mock, stub, spy) dibahas di chapter Test Doubles .

Ringkasan #

Dependency Inversion menutup rangkaian SOLID dengan membalik arah panah ketergantungan: aturan bisnis mendefinisikan kontrak, detail teknis mengimplementasikannya, dan keduanya dipertemukan di composition root lewat dependency injection. Imbalannya konkret — layanan bisa diuji tanpa infrastruktur, perpindahan teknis tidak menyentuh kode bisnis, dan istilah DI, DIP, dan DIC tidak lagi tertukar. Dengan lima prinsip SOLID lengkap, kita siap masuk ke dunia design pattern, dimulai dari yang paling mendasar: Factory Pattern.

Referensi #