Injeksi Ketergantungan dalam Go: Pola & Praktik Terbaik

Master pola DI untuk kode Go yang dapat diuji

Konten Halaman

Injeksi ketergantungan (DI) adalah pola desain yang fundamental yang mendorong kode yang bersih, dapat diuji, dan mudah dipelihara dalam aplikasi Go.

Apakah Anda sedang membangun API REST, menerapkan pola basis data multi-tenant, atau bekerja dengan pustaka ORM, memahami injeksi ketergantungan akan secara signifikan meningkatkan kualitas kode Anda.

go-dependency-injection

Apa itu Injeksi Ketergantungan?

Injeksi ketergantungan adalah pola desain di mana komponen menerima ketergantungan mereka dari sumber eksternal daripada menciptakannya secara internal. Pendekatan ini memisahkan komponen, membuat kode Anda lebih modular, dapat diuji, dan mudah dipelihara.

Di Go, injeksi ketergantungan sangat kuat karena filosofi desain berbasis antarmuka dari bahasa tersebut. Kepuasan antarmuka yang implisit di Go berarti Anda dapat dengan mudah menukar implementasi tanpa memodifikasi kode yang sudah ada.

Mengapa Menggunakan Injeksi Ketergantungan di Go?

Peningkatan Kemudahan Pengujian: Dengan menginjeksikan ketergantungan, Anda dapat dengan mudah mengganti implementasi nyata dengan mock atau double pengujian. Ini memungkinkan Anda menulis pengujian unit yang cepat, terisolasi, dan tidak memerlukan layanan eksternal seperti basis data atau API.

Pemeliharaan yang Lebih Baik: Ketergantungan menjadi eksplisit dalam kode Anda. Ketika Anda melihat fungsi konstruktor, Anda segera melihat apa yang diperlukan oleh komponen. Ini membuat kodebase lebih mudah dipahami dan dimodifikasi.

Kopling yang Lebih Rendah: Komponen bergantung pada abstraksi (antarmuka) daripada implementasi konkret. Ini berarti Anda dapat mengubah implementasi tanpa memengaruhi kode yang bergantung.

Fleksibilitas: Anda dapat mengatur implementasi yang berbeda untuk lingkungan yang berbeda (pengembangan, pengujian, produksi) tanpa mengubah logika bisnis Anda.

Injeksi Konstruktor: Cara Go

Cara paling umum dan idiomatic untuk menerapkan injeksi ketergantungan di Go adalah melalui fungsi konstruktor. Ini biasanya diberi nama NewXxx dan menerima ketergantungan sebagai parameter.

Contoh Dasar

Berikut adalah contoh sederhana yang menunjukkan injeksi konstruktor:

// Definisikan antarmuka untuk repositori
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// Layanan bergantung pada antarmuka repositori
type UserService struct {
    repo UserRepository
}

// Konstruktor menginjeksikan ketergantungan
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// Metode menggunakan ketergantungan yang diinjeksikan
func (s *UserService) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

Polanya ini membuat jelas bahwa UserService membutuhkan UserRepository. Anda tidak dapat menciptakan UserService tanpa menyediakan repositori, yang mencegah kesalahan runtime dari ketergantungan yang hilang.

Banyak Ketergantungan

Ketika komponen memiliki banyak ketergantungan, cukup tambahkan sebagai parameter konstruktor:

type EmailService interface {
    Send(to, subject, body string) error
}

type Logger interface {
    Info(msg string)
    Error(msg string, err error)
}

type OrderService struct {
    repo        OrderRepository
    emailSvc    EmailService
    logger      Logger
    paymentSvc  PaymentService
}

func NewOrderService(
    repo OrderRepository,
    emailSvc EmailService,
    logger Logger,
    paymentSvc PaymentService,
) *OrderService {
    return &OrderService{
        repo:       repo,
        emailSvc:   emailSvc,
        logger:     logger,
        paymentSvc: paymentSvc,
    }
}

Desain Antarmuka untuk Injeksi Ketergantungan

Salah satu prinsip kunci saat menerapkan injeksi ketergantungan adalah Prinsip Inversi Ketergantungan (DIP): modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah; keduanya harus bergantung pada abstraksi.

Di Go, ini berarti mendefinisikan antarmuka kecil dan fokus yang hanya merepresentasikan apa yang dibutuhkan komponen Anda:

// Baik: Antarmuka kecil dan fokus
type PaymentProcessor interface {
    ProcessPayment(amount float64) error
}

// Buruk: Antarmuka besar dengan metode yang tidak perlu
type PaymentService interface {
    ProcessPayment(amount float64) error
    RefundPayment(id string) error
    GetPaymentHistory(userID int) ([]Payment, error)
    UpdatePaymentMethod(userID int, method PaymentMethod) error
    // ... banyak metode lainnya
}

Antarmuka yang lebih kecil mengikuti Prinsip Segregasi Antarmuka—klien tidak boleh bergantung pada metode yang tidak mereka gunakan. Ini membuat kode Anda lebih fleksibel dan lebih mudah diuji.

Contoh Nyata: Abstraksi Basis Data

Ketika bekerja dengan basis data dalam aplikasi Go, Anda sering perlu mengabstraksi operasi basis data. Berikut adalah bagaimana injeksi ketergantungan membantu:

// Antarmuka basis data - abstraksi tingkat tinggi
type DB interface {
    Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
    Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
    BeginTx(ctx context.Context) (Tx, error)
}

// Repositori bergantung pada abstraksi
type UserRepository struct {
    db DB
}

func NewUserRepository(db DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    query := "SELECT id, name, email FROM users WHERE id = $1"
    rows, err := r.db.Query(ctx, query, id)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    // ... parsing rows
}

Polanya ini sangat berguna ketika menerapkan pola basis data multi-tenant, di mana Anda mungkin perlu beralih antara implementasi basis data yang berbeda atau strategi koneksi.

Pola Akar Komposisi

Akar Komposisi adalah tempat di mana Anda menggabungkan semua ketergantungan di titik masuk aplikasi (biasanya main). Ini memusatkan konfigurasi ketergantungan dan membuat grafik ketergantungan menjadi eksplisit.

func main() {
    // Inisialisasi ketergantungan infrastruktur
    db := initDatabase()
    logger := initLogger()
    
    // Inisialisasi repositori
    userRepo := NewUserRepository(db)
    orderRepo := NewOrderRepository(db)
    
    // Inisialisasi layanan dengan ketergantungan
    emailSvc := NewEmailService(logger)
    paymentSvc := NewPaymentService(logger)
    userSvc := NewUserService(userRepo, logger)
    orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
    
    // Inisialisasi penangan HTTP
    userHandler := NewUserHandler(userSvc)
    orderHandler := NewOrderHandler(orderSvc)
    
    // Hubungkan rute
    router := setupRouter(userHandler, orderHandler)
    
    // Mulai server
    log.Fatal(http.ListenAndServe(":8080", router))
}

Pendekatan ini membuat jelas bagaimana aplikasi Anda dirancang dan dari mana ketergantungan berasal. Ini sangat bernilai ketika membangun API REST di Go, di mana Anda perlu mengkoordinasikan lapisan ketergantungan yang banyak.

Kerangka Kerja Injeksi Ketergantungan

Untuk aplikasi yang lebih besar dengan grafik ketergantungan kompleks, mengelola ketergantungan secara manual dapat menjadi melelahkan. Go memiliki beberapa kerangka kerja DI yang dapat membantu:

Google Wire (DI Waktu Kompilasi)

Wire adalah alat injeksi ketergantungan waktu kompilasi yang menghasilkan kode. Ini aman jenis dan tidak memiliki biaya runtime.

Instalasi:

go install github.com/google/wire/cmd/wire@latest

Contoh:

// wire.go
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewUserRepository,
        NewUserService,
        NewUserHandler,
        NewApp,
    )
    return &App{}, nil
}

Wire menghasilkan kode injeksi ketergantungan di waktu kompilasi, memastikan keamanan jenis dan menghilangkan biaya overhead refleksi runtime.

Uber Dig (DI Runtime)

Dig adalah kerangka injeksi ketergantungan runtime yang menggunakan refleksi. Ini lebih fleksibel tetapi memiliki biaya runtime tertentu.

Contoh:

import "go.uber.org/dig"

func main() {
    container := dig.New()
    
    // Daftarkan penyedia
    container.Provide(NewDB)
    container.Provide(NewUserRepository)
    container.Provide(NewUserService)
    container.Provide(NewUserHandler)
    
    // Pemanggilan fungsi yang membutuhkan ketergantungan
    err := container.Invoke(func(handler *UserHandler) {
        // Gunakan handler
    })
    if err != nil {
        log.Fatal(err)
    }
}

Kapan Menggunakan Kerangka Kerja

Gunakan kerangka kerja ketika:

  • Grafik ketergantungan Anda kompleks dengan banyak komponen yang saling bergantung
  • Anda memiliki banyak implementasi dari antarmuka yang sama yang perlu dipilih berdasarkan konfigurasi
  • Anda ingin resolusi ketergantungan otomatis
  • Anda membangun aplikasi besar di mana pengkabelan manual menjadi rentan terhadap kesalahan

Tetapkan injeksi manual ketika:

  • Aplikasi Anda kecil hingga sedang
  • Grafik ketergantungan Anda sederhana dan mudah diikuti
  • Anda ingin menjaga ketergantungan minimal dan eksplisit
  • Anda lebih memilih kode eksplisit daripada kode yang dihasilkan

Pengujian dengan Injeksi Ketergantungan

Salah satu manfaat utama dari injeksi ketergantungan adalah peningkatan kemudahan pengujian. Berikut adalah cara DI membuat pengujian lebih mudah:

Contoh Pengujian Unit

// Implementasi mock untuk pengujian
type mockUserRepository struct {
    users map[int]*User
    err   error
}

func (m *mockUserRepository) FindByID(id int) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    return m.users[id], nil
}

func (m *mockUserRepository) Save(user *User) error {
    if m.err != nil {
        return m.err
    }
    m.users[user.ID] = user
    return nil
}

// Pengujian menggunakan mock
func TestUserService_GetUser(t *testing.T) {
    mockRepo := &mockUserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "John", Email: "john@example.com"},
        },
    }
    
    service := NewUserService(mockRepo)
    
    user, err := service.GetUser(1)
    assert.NoError(t, err)
    assert.Equal(t, "John", user.Name)
}

Pengujian ini berjalan cepat, tidak memerlukan basis data, dan menguji logika bisnis Anda secara terisolasi. Ketika bekerja dengan pustaka ORM di Go, Anda dapat menyuntikkan repositori mock untuk menguji logika layanan tanpa konfigurasi basis data.

Pola Umum dan Praktik Terbaik

1. Gunakan Segregasi Antarmuka

Jaga antarmuka tetap kecil dan fokus pada apa yang klien butuhkan:

// Baik: Klien hanya membutuhkan membaca pengguna
type UserReader interface {
    FindByID(id int) (*User, error)
    FindByEmail(email string) (*User, error)
}

// Antarmuka terpisah untuk menulis
type UserWriter interface {
    Save(user *User) error
    Delete(id int) error
}

2. Kembalikan Kesalahan dari Konstruktor

Konstruktor harus memvalidasi ketergantungan dan mengembalikan kesalahan jika inisialisasi gagal:

func NewUserService(repo UserRepository) (*UserService, error) {
    if repo == nil {
        return nil, errors.New("user repository cannot be nil")
    }
    return &UserService{repo: repo}, nil
}

3. Gunakan Konteks untuk Ketergantungan Berbasis Permintaan

Untuk ketergantungan yang spesifik permintaan (seperti transaksi basis data), kirimkan melalui konteks:

type ctxKey string

const dbKey ctxKey = "db"

func WithDB(ctx context.Context, db DB) context.Context {
    return context.WithValue(ctx, dbKey, db)
}

func DBFromContext(ctx context.Context) (DB, bool) {
    db, ok := ctx.Value(dbKey).(DB)
    return db, ok
}

4. Hindari Injeksi Berlebihan

Jangan injeksikan ketergantungan yang benar-benar detail implementasi internal. Jika komponen menciptakan dan mengelola objek bantu sendiri, itu baik-baik saja:

// Baik: Objek bantu internal tidak perlu injeksi
type UserService struct {
    repo UserRepository
    // Cache internal - tidak perlu injeksi
    cache map[int]*User
}

func NewUserService(repo UserRepository) *UserService {
    return &UserService{
        repo:  repo,
        cache: make(map[int]*User),
    }
}

5. Dokumentasikan Ketergantungan

Gunakan komentar untuk mendokumentasikan mengapa ketergantungan diperlukan dan setiap keterbatasan:

// UserService menangani logika bisnis terkait pengguna.
// Ia memerlukan UserRepository untuk akses data dan Logger untuk
// pelacakan kesalahan. Repository harus aman thread jika digunakan
// dalam konteks konkuren.
func NewUserService(repo UserRepository, logger Logger) *UserService {
    // ...
}

Kapan Tidak Menggunakan Injeksi Ketergantungan

Injeksi ketergantungan adalah alat yang kuat, tetapi tidak selalu diperlukan:

Lewatkan DI untuk:

  • Objek nilai sederhana atau struktur data
  • Fungsi bantu internal atau utilitas
  • Skrip satu kali atau utilitas kecil
  • Ketika instansiasi langsung lebih jelas dan sederhana

Contoh kapan tidak menggunakan DI:

// Struktur sederhana - tidak perlu DI
type Point struct {
    X, Y float64
}

func NewPoint(x, y float64) Point {
    return Point{X: x, Y: y}
}

// Utilitas sederhana - tidak perlu DI
func FormatCurrency(amount float64) string {
    return fmt.Sprintf("$%.2f", amount)
}

Integrasi dengan Ekosistem Go

Injeksi ketergantungan bekerja secara sejajar dengan pola dan alat lainnya di Go. Ketika membangun aplikasi yang menggunakan perpustakaan standar Go untuk pengambilan data web atau membuat laporan PDF, Anda dapat menyuntikkan layanan ini ke dalam logika bisnis Anda:

type PDFGenerator interface {
    GenerateReport(data ReportData) ([]byte, error)
}

type ReportService struct {
    pdfGen PDFGenerator
    repo   ReportRepository
}

func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
    return &ReportService{
        pdfGen: pdfGen,
        repo:   repo,
    }
}

Ini memungkinkan Anda mengganti implementasi pembuatan PDF atau menggunakan mock selama pengujian.

Kesimpulan

Injeksi ketergantungan adalah fondasi dari menulis kode Go yang dapat dipelihara dan diuji. Dengan mengikuti pola yang dijelaskan dalam artikel ini—konstruktor injeksi, desain berbasis antarmuka, dan pola akar komposisi—Anda akan menciptakan aplikasi yang lebih mudah dipahami, diuji, dan dimodifikasi.

Mulailah dengan injeksi konstruktor manual untuk aplikasi kecil hingga sedang, dan pertimbangkan kerangka kerja seperti Wire atau Dig seiring grafik ketergantungan Anda berkembang. Ingat bahwa tujuannya adalah kejelasan dan kemudahan pengujian, bukan kompleksitas untuk sendirinya.

Untuk sumber daya pengembangan Go tambahan, lihat Kartu Panduan Go.

Tautan Berguna

Sumber Eksternal