158. Konsep Dependency Injection & Loose Coupling
Memahami pentingnya pemisahan dependensi (Inversion of Control) untuk kode yang modular dan testable.
Apa itu Dependency Injection (DI)?
Dependency Injection adalah teknik desain perangkat lunak di mana sebuah objek (atau struct) menerima objek lain yang dibutuhkannya (dependensinya) dari luar, bukannya membuat sendiri dependensi tersebut di dalam constructor-nya.
// TANPA DI (Tightly Coupled - Buruk):
type UserService struct {
repo *MySQLUserRepository // Terikat mati ke MySQL! Susah di-unit test
}
func NewUserService() *UserService {
return &UserService{repo: &MySQLUserRepository{}}
}
type UserService struct {
repo UserRepository // Bergantung pada Interface!
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
Contoh program
package main
import (
"fmt"
)
type Repository interface {
GetData() string
}
type MemoryRepo struct{}
func (r *MemoryRepo) GetData() string { return "Data dari Memory DB" }
type Service struct {
repo Repository // Injeksi via Interface
}
func NewService(r Repository) *Service {
return &Service{repo: r}
}
func main() {
repo := &MemoryRepo{}
svc := NewService(repo) // Dependency Injection Manual
fmt.Println("Service Result:", svc.repo.GetData())
}
159. Pengenalan Google Wire (Compile-Time DI Generator)
Mengenal Google Wire sebagai tool generator kode Dependency Injection tanpa reflection saat runtime.
Mengapa Google Wire?
Di bahasa pemrograman seperti Java (Spring) atau C# (.NET), Dependency Injection biasa menggunakan reflection saat aplikasi dinyalakan. Namun pada Go, reflection dapat memperlambat waktu start-up dan menimbulkan error di runtime.
- Compile-Time Code Generation: Wire membuat kode Go standar (`wire_gen.go`) saat kompilasi.
- Tanpa Reflection: Sangat cepat dan zero overhead saat aplikasi dijalankan.
- Type-Safe: Jika ada dependensi yang kurang atau tipe data tidak cocok, kesalahan langsung terdeteksi saat
wiredi-run!
Instalasi CLI Wire:
go install github.com/google/wire/cmd/wire@latest
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== DEMO GOOGLE WIRE ===")
fmt.Println("Google Wire adalah Compile-Time Dependency Injection tool buatan Google.")
fmt.Println("Keunggulan: Bebas Reflection, Type-Safe, dan Terverifikasi saat Compile Time!")
}
160. Provider & Constructor Function Pattern
Memahami konsep Provider sebagai fungsi constructor pengembali instance dependensi.
Apa itu Provider di Wire?
Provider adalah fungsi Go biasa yang menghasilkan sebuah nilai atau instance dependensi. Fungsi constructor standar Go (seperti NewUserRepository atau NewDBConnection) secara otomatis dianggap sebagai Provider oleh Google Wire.
// Provider DB Connection
func ProvideDB() *sql.DB {
db, _ := sql.Open("mysql", "root:root@tcp(localhost:3306)/app")
return db
}
// Provider Repository (Membutuhkan DB)
func ProvideUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{DB: db}
}
Contoh program
package main
import (
"fmt"
)
type Database struct{ Dsn string }
type UserRepo struct{ DB *Database }
// Provider 1
func NewDatabase() *Database {
return &Database{Dsn: "localhost:3306"}
}
// Provider 2 (Membutuhkan *Database)
func NewUserRepo(db *Database) *UserRepo {
return &UserRepo{DB: db}
}
func main() {
db := NewDatabase()
repo := NewUserRepo(db)
fmt.Println("Repo DB DSN:", repo.DB.Dsn)
}
161. Injector Function & File wire.go
Deklarasi fungsi Injector menggunakan `wire.Build(...)` dalam file khusus `injector.go` atau `wire.go`.
Membuat File Injector (`wire.go`)
Injector adalah fungsi yang merangkai provider-provider untuk mengembalikan objek akhir aplikasi. Kita menuliskan fungsi Injector di file wire.go dengan directive build tag khusus //go:build wireinject.
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func InitializedUserService() *UserService {
wire.Build(ProvideDB, ProvideUserRepository, ProvideUserService)
return nil
}
Saat perintah wire dijalankan di terminal, Wire akan menghasilkan file wire_gen.go berisi kode perangkaian otomatis!
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== INJECTOR DEMO ===")
fmt.Println("File wire.go berisi deklarasi wire.Build(Provider1, Provider2...)")
fmt.Println("Command 'wire' menghasilkan file wire_gen.go!")
}
162. Interface Binding dengan wire.Bind
Mengabaikan detail konkret struct dan mengaitkan implementasi struct ke Interface menggunakan `wire.Bind`.
Interface Binding (`wire.Bind`)
Jika sebuah Service membutuhkan dependensi bertipe Interface, sedangkan Provider mengembalikan pointer *StructKonkret, Wire perlu tahu bahwa struct tersebut mengimplementasikan interface tersebut.
var UserRepositorySet = wire.NewSet(
ProvideMySQLUserRepository,
wire.Bind(new(UserRepository), new(*MySQLUserRepository)),
)
Fungsi wire.Bind(new(TargetInterface), new(*ConcreteStruct)) memberitahu Wire untuk menginjeksi struct konkret sebagai interface target.
Contoh program
package main
import (
"fmt"
)
type CategoryRepo interface {
FindAll() []string
}
type MemoryCategoryRepo struct{}
func (m *MemoryCategoryRepo) FindAll() []string {
return []string{"Elektronik", "Pakaian", "Buku"}
}
func main() {
var repo CategoryRepo = &MemoryCategoryRepo{}
fmt.Println("Categories:", repo.FindAll())
}
163. Provider Sets (wire.NewSet)
Mengelompokkan provider-provider terkait menjadi satu modul terorganisir menggunakan `wire.NewSet`.
Pengelompokan Provider (Provider Set)
Pada aplikasi berskala besar dengan puluhan modul, mendaftarkan setiap provider satu per satu di `wire.Build(...)` akan sangat panjang. Wire menyediakan wire.NewSet untuk mengelompokkan provider yang saling berhubungan.
var UserSet = wire.NewSet(
ProvideUserRepository,
ProvideUserService,
ProvideUserController,
)
Selanjutnya pada `wire.go` kita cukup memanggil wire.Build(UserSet, OrderSet, DatabaseSet).
Contoh program
package main
import (
"fmt"
)
func main() {
fmt.Println("=== PROVIDER SETS DEMO ===")
fmt.Println("wire.NewSet(Provider1, Provider2, Provider3) merangkai grup provider menjadi modul terstruktur.")
}
164. Struct Provider & Property Injection (wire.Struct)
Melakukan injeksi dependensi langsung ke field struct tanpa perlu membuat fungsi constructor manual.
Injeksi Langsung Field Struct (`wire.Struct`)
Jika sebuah struct hanya berfungsi menampung dependensi tanpa ada logika inisialisasi kompleks di constructor, kita dapat menggunakan wire.Struct daripada membuat fungsi New...() secara manual.
// Injeksi ke seluruh field struct: var FooSet = wire.Struct(new(Foo), "*") // Injeksi spesifik field tertentu saja: var BarSet = wire.Struct(new(Bar), "MyRepo", "MyLogger")
Contoh program
package main
import (
"fmt"
)
type DB struct{}
type Logger struct{}
type AppService struct {
DB *DB
Logger *Logger
}
func main() {
// Simulasi wire.Struct(new(AppService), "*")
app := &AppService{
DB: &DB{},
Logger: &Logger{},
}
fmt.Printf("AppService initialized with DB (%T) and Logger (%T)\n", app.DB, app.Logger)
}
165. Cleanup Functions & Error Handling di Wire
Menangani error return dari Provider dan mengelola fungsi penutupan resource (Cleanup/Close) di Wire.
Error Handling & Cleanup Functions
Provider di Wire dapat mengembalikan error atau fungsi cleanup dengan signature func().
func ProvideFile() (*os.File, func(), error) {
file, err := os.OpenFile("data.txt", os.O_RDWR, 0644)
if err != nil {
return nil, nil, err
}
cleanup := func() {
file.Close() // Dipanggil saat aplikasi shutdown
}
return file, cleanup, nil
}
Wire akan secara otomatis meneruskan penanganan error ke Injector dan merangkai fungsi-fungsi cleanup untuk dipanggil di main()!
Contoh program
package main
import (
"fmt"
)
func InitResource() (string, func(), error) {
resource := "Connection Established"
cleanup := func() {
fmt.Println("🧹 CLEANUP: Resources closed successfully!")
}
return resource, cleanup, nil
}
func main() {
res, cleanup, err := InitResource()
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Resource Status:", res)
// Eksekusi cleanup di akhir main
defer cleanup()
}