Aplikasi yang tumbuh biasanya berlapis: layanan bisnis di atas, penyimpanan data di bawah. Masalah klasiknya muncul diam-diam — layanan mulai memanggil database secara langsung, hingga setiap perubahan schema ikut mengguncang kode bisnis, dan menguji satu fungsi registrasi menuntut database yang hidup. Prinsip dependency inversion membalik keadaan ini: modul level atas berhenti bergantung pada modul level bawah, dan keduanya justru bergantung pada satu abstraksi. Chapter ini menerapkan prinsip tersebut dengan interface — penutup yang sekaligus merangkai semua kontrak yang telah kita pelajari di section ini.

Masalah: Layanan yang Mengenal Database #

Bayangkan LayananPengguna yang memanggil SQL langsung di dalam method Daftarkan. Dalam desain seperti itu, arah dependensi mengikuti arah pemanggilan: layanan tahu nama tabel, bentuk query, dan detail driver database. Tiga keluhan lahir dari arah ini. Pertama, mengganti penyimpanan berarti menyunting modul bisnis. Kedua, pengujian tidak bisa berjalan tanpa infrastruktur nyata. Ketiga, pembaca kode bisnis harus memahami detail teknis yang seharusnya bukan urusannya.

Membalik Arah dengan Abstraksi #

Prinsip dependency inversion menyatakan dua hal: modul level tinggi tidak boleh bergantung pada modul level rendah, dan keduanya harus bergantung pada abstraksi. Di Go, abstraksi itu adalah interface kecil yang dideklarasikan di sisi pemakai — persis aturan the interface goes where it is used dari chapter Kecilnya Interface . LayananPengguna mendeklarasikan UserRepository berisi dua method yang benar-benar dipanggilnya: Simpan dan Cari.

Setelah dibalik, arah dependensi berubah total:

Layanan hanya mengenal UserRepository. MemoryRepository (dan kelak PostgresRepository) mengimplementasikan interface secara implisit tanpa mengimpor layanan. Kedua sisi bertemu di abstraksi, sehingga detail penyimpanan boleh berganti tanpa menyentuh kode bisnis.

Fake untuk Pengujian #

Manfaat pembalikan ini paling terasa saat menguji. Implementasi tiruan — fake — memenuhi UserRepository tanpa menyentuh map, file, atau jaringan: fakeRepo pada demo di bawah hanya menghitung pemanggilan dan mengembalikan nilai tetap. Modul bisnis yang sama kini bisa diuji murni: validasi di Daftarkan teruji tanpa database, dan jumlah pemanggilan repository bisa dibandingkan dengan ekspektasi test. Pemakaian fake yang lebih realistis dengan go test dibahas pada chapter Menguji Package dengan Dependency .

Program Demo #

Berkas dip-demo.go menyusun satu layanan, dua repository, dan dua jalur eksekusi:

go
package main

import (
	"errors"
	"fmt"
)

// Pengguna adalah data domain milik modul high-level.
type Pengguna struct {
	ID    int
	Email string
}

// UserRepository adalah abstraksi penyimpanan yang dibutuhkan layanan.
type UserRepository interface {
	Simpan(p Pengguna) error
	Cari(id int) (Pengguna, error)
}

// MemoryRepository adalah implementasi low-level berbasis map.
type MemoryRepository struct {
	data map[int]Pengguna
}

func NewMemoryRepository() *MemoryRepository {
	return &MemoryRepository{data: make(map[int]Pengguna)}
}

func (m *MemoryRepository) Simpan(p Pengguna) error {
	m.data[p.ID] = p
	return nil
}

func (m *MemoryRepository) Cari(id int) (Pengguna, error) {
	p, ok := m.data[id]
	if !ok {
		return Pengguna{}, fmt.Errorf("pengguna %d tidak ditemukan", id)
	}
	return p, nil
}

// LayananPengguna adalah modul high-level: hanya mengenal abstraksi.
type LayananPengguna struct {
	repo UserRepository
}

func (s *LayananPengguna) Daftarkan(id int, email string) error {
	if id <= 0 {
		return errors.New("id harus positif")
	}
	return s.repo.Simpan(Pengguna{ID: id, Email: email})
}

func (s *LayananPengguna) AmbilEmail(id int) (string, error) {
	p, err := s.repo.Cari(id)
	if err != nil {
		return "", err
	}
	return p.Email, nil
}

// fakeRepo adalah pengganti uji: mencatat pemanggilan tanpa menyimpan data.
type fakeRepo struct {
	terpanggil int
}

func (f *fakeRepo) Simpan(p Pengguna) error {
	f.terpanggil++
	return nil
}

func (f *fakeRepo) Cari(id int) (Pengguna, error) {
	f.terpanggil++
	return Pengguna{ID: id, Email: "[email protected]"}, nil
}

func main() {
	// Jalur produksi: layanan berjalan di atas repository memori.
	layanan := &LayananPengguna{repo: NewMemoryRepository()}

	if err := layanan.Daftarkan(0, "[email protected]"); err != nil {
		fmt.Println("validasi menolak:", err)
	}
	if err := layanan.Daftarkan(7, "[email protected]"); err != nil {
		fmt.Println("gagal menyimpan:", err)
	}
	email, err := layanan.AmbilEmail(7)
	fmt.Println("email:", email, "err:", err)

	// Jalur pengujian: layanan yang sama diuji dengan fakeRepo.
	fake := &fakeRepo{}
	layananUji := &LayananPengguna{repo: fake}
	_ = layananUji.Daftarkan(1, "[email protected]")
	emailUji, _ := layananUji.AmbilEmail(1)
	fmt.Println("email dari fake:", emailUji)
	fmt.Println("jumlah pemanggilan repo:", fake.terpanggil)
}

Output yang Dihasilkan:

text
validasi menolak: id harus positif
email: [email protected] err: <nil>
email dari fake: [email protected]
jumlah pemanggilan repo: 2

Mari kita telaah baris demi baris program di atas:

  1. Baris 15–18 (type UserRepository interface): abstraksi dideklarasikan dari sudut pandang layanan; hanya dua method yang benar-benar dipakai, sesuai prinsip interface kecil.
  2. Baris 25–27 (NewMemoryRepository): constructor menyembunyikan detail map dari pemanggil; modul level bawah punya urusan internalnya sendiri.
  3. Baris 29–40 (Simpan, Cari milik MemoryRepository): implementasi nyata memakai map — mudah diganti dengan SQL tanpa menyentuh layanan.
  4. Baris 43–45 (type LayananPengguna struct): layanan hanya menyimpan field bertipe interface; nama MemoryRepository tidak muncul di sini.
  5. Baris 47–52 (Daftarkan): aturan bisnis (id harus positif) teruji murni karena penyimpanan tinggal satu baris di akhir method.
  6. Baris 62–75 (fakeRepo): fake memenuhi interface secara implisit; terpanggil menjadi pencatat interaksi yang bisa diperiksa test.
  7. Baris 77–97 (func main): jalur produksi memakai repository memori, jalur uji memakai fake; kedua LayananPengguna dibangun dengan cara yang sama persis.

Dependency inversion menuntup section interface dengan bukti bahwa kontrak perilaku bukan konsep akademis: satu interface kecil cukup untuk melepas modul bisnis dari infrastruktur, sekaligus membuatnya teruji tanpa prasyarat. Pada section berikutnya kita berkenalan dengan interface paling terkenal di Go — error — beserta seluruh taktik penanganannya di Section Error Handling .

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

dip-demo.go

Unduh berkas
go
package main

import (
	"errors"
	"fmt"
)

// Pengguna adalah data domain milik modul high-level.
type Pengguna struct {
	ID    int
	Email string
}

// UserRepository adalah abstraksi penyimpanan yang dibutuhkan layanan.
type UserRepository interface {
	Simpan(p Pengguna) error
	Cari(id int) (Pengguna, error)
}

// MemoryRepository adalah implementasi low-level berbasis map.
type MemoryRepository struct {
	data map[int]Pengguna
}

func NewMemoryRepository() *MemoryRepository {
	return &MemoryRepository{data: make(map[int]Pengguna)}
}

func (m *MemoryRepository) Simpan(p Pengguna) error {
	m.data[p.ID] = p
	return nil
}

func (m *MemoryRepository) Cari(id int) (Pengguna, error) {
	p, ok := m.data[id]
	if !ok {
		return Pengguna{}, fmt.Errorf("pengguna %d tidak ditemukan", id)
	}
	return p, nil
}

// LayananPengguna adalah modul high-level: hanya mengenal abstraksi.
type LayananPengguna struct {
	repo UserRepository
}

func (s *LayananPengguna) Daftarkan(id int, email string) error {
	if id <= 0 {
		return errors.New("id harus positif")
	}
	return s.repo.Simpan(Pengguna{ID: id, Email: email})
}

func (s *LayananPengguna) AmbilEmail(id int) (string, error) {
	p, err := s.repo.Cari(id)
	if err != nil {
		return "", err
	}
	return p.Email, nil
}

// fakeRepo adalah pengganti uji: mencatat pemanggilan tanpa menyimpan data.
type fakeRepo struct {
	terpanggil int
}

func (f *fakeRepo) Simpan(p Pengguna) error {
	f.terpanggil++
	return nil
}

func (f *fakeRepo) Cari(id int) (Pengguna, error) {
	f.terpanggil++
	return Pengguna{ID: id, Email: "[email protected]"}, nil
}

func main() {
	// Jalur produksi: layanan berjalan di atas repository memori.
	layanan := &LayananPengguna{repo: NewMemoryRepository()}

	if err := layanan.Daftarkan(0, "[email protected]"); err != nil {
		fmt.Println("validasi menolak:", err)
	}
	if err := layanan.Daftarkan(7, "[email protected]"); err != nil {
		fmt.Println("gagal menyimpan:", err)
	}
	email, err := layanan.AmbilEmail(7)
	fmt.Println("email:", email, "err:", err)

	// Jalur pengujian: layanan yang sama diuji dengan fakeRepo.
	fake := &fakeRepo{}
	layananUji := &LayananPengguna{repo: fake}
	_ = layananUji.Daftarkan(1, "[email protected]")
	emailUji, _ := layananUji.AmbilEmail(1)
	fmt.Println("email dari fake:", emailUji)
	fmt.Println("jumlah pemanggilan repo:", fake.terpanggil)
}