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:
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:
validasi menolak: id harus positif
email: [email protected] err: <nil>
email dari fake: [email protected]
jumlah pemanggilan repo: 2Mari kita telaah baris demi baris program di atas:
- Baris 15–18 (
type UserRepository interface): abstraksi dideklarasikan dari sudut pandang layanan; hanya dua method yang benar-benar dipakai, sesuai prinsip interface kecil. - Baris 25–27 (
NewMemoryRepository): constructor menyembunyikan detailmapdari pemanggil; modul level bawah punya urusan internalnya sendiri. - Baris 29–40 (
Simpan,CarimilikMemoryRepository): implementasi nyata memakai map — mudah diganti dengan SQL tanpa menyentuh layanan. - Baris 43–45 (
type LayananPengguna struct): layanan hanya menyimpan field bertipe interface; namaMemoryRepositorytidak muncul di sini. - Baris 47–52 (
Daftarkan): aturan bisnis (id harus positif) teruji murni karena penyimpanan tinggal satu baris di akhir method. - Baris 62–75 (
fakeRepo): fake memenuhi interface secara implisit;terpanggilmenjadi pencatat interaksi yang bisa diperiksa test. - Baris 77–97 (
func main): jalur produksi memakai repository memori, jalur uji memakai fake; keduaLayananPenggunadibangun 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 #
- Effective Go: Interfaces — pola pemakaian interface untuk memisahkan modul.
- Dokumentasi package io — contoh abstraksi penyimpanan yang didefinisikan dari sisi pemakai.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
dip-demo.go
Unduh berkaspackage 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)
}