tutorial

62 Resolver dengan Dependency Injection di Go

62 Resolver dengan Dependency Injection di Go

Dependency Injection (DI) di Go adalah praktik penting yang mendukung testing, scalability, dan pemisahan concern. Namun, integrasi teknik DI dengan provider dependency—seperti resolver pada GraphQL—masih sering membingungkan, terutama untuk engineer Go yang terbiasa dengan struktur kode monolitik. Dalam artikel ini, saya akan membahas penerapan 62 resolver dengan Dependency Injection di Go, membedah konsep ini, menjelaskan best-practices, dan melengkapi dengan contoh kode, diagram, serta tabel simulasi. Mari kita mulai.


Apa Itu Resolver dalam Konteks Go?

Pada umumnya, resolver merujuk pada fungsi/factory yang “meresolusikan” dependensi runtime saat code berjalan. Contoh paling nyata terlihat pada aplikasi GraphQL di Go, misal menggunakan gqlgen. Resolver merepresentasikan layer antara query GraphQL dan implementation business logic, seringkali bergantung pada service, repository atau komponen lain.

Angka 62 di judul menunjuk pada jumlah resolver yang kita simulasikan di skala proyek menengah-besar (misal: 62 endpoint/service logic yang perlu “diresolusikan”).


Tantangan Mengelola 62 Resolver secara Manual

Masalah utama ketika mengelola banyak resolver di Go:

  1. Duplikasi kode boilerplate: Injeksi dependensi secara manual di setiap resolver.
  2. Sulit di-maintain: Setiap penambahan dependency baru, mengubah constructor tiap resolver.
  3. Testing lebih susah: Kalau DI tidak konsisten, mocking dan testing jadi rumit.

Dependency Injection dasar di Go

Go bukan Java atau .NET yang memiliki DI container secara native. Namun, praktik dependency injection tetap bisa digunakan dengan prinsip constructor injection atau interface injection.

Berikut contoh pola paling dasar:

type UserService interface {
    GetUser(id string) (*User, error)
}

type UserResolver struct {
    Service UserService
}

func NewUserResolver(svc UserService) *UserResolver {
    return &UserResolver{Service: svc}
}

Untuk 62 resolver, tanpa DI yang baik:

var postResolver   = NewPostResolver(postService)
var commentResolver = NewCommentResolver(commentService)
var friendResolver  = NewFriendResolver(friendService)
// dst untuk ke-62 resolver...

Terlalu verbose dan mudah terjadi kesalahan.


Membuat Layer Dependency Injection yang Efektif

Strategi populer untuk resolver injection di Go:

  • Manual wiring — Langsung menghubungkan dependency pada main.go atau composition root
  • Menggunakan DI container — Library third-party seperti uber-go/dig

Mari bahas keduanya.

1. Manual Wiring

Paling simple dan “Go idiomatic”. Tabel berikut membandingkan manual wiring dan container:

KriteriaManual WiringDI Container (dig)
BoilerplateCukup BanyakMinim
Compile-time SafetyTinggiSedang
Refactor MudahTergantungMudah
Learning CurveRendahSedang

Contoh Manual Wiring (Constructor Injection)

Misal kita punya 3 service dan ingin extend ke 62 resolver (kode berikut prinsip dasarnya):

type AppResolvers struct {
    UserResolver    *UserResolver
    PostResolver    *PostResolver
    CommentResolver *CommentResolver
    // ... sampai 62 resolver
}

func NewAppResolvers(db *sql.DB) *AppResolvers {
    userService := NewUserService(db)
    postService := NewPostService(db)
    commentService := NewCommentService(db)

    return &AppResolvers{
        UserResolver:    NewUserResolver(userService),
        PostResolver:    NewPostResolver(postService),
        CommentResolver: NewCommentResolver(commentService),
    }
}

Jika dependency chain semakin dalam (misal service tergantung repository, tergantung cache dll), wiring jadi semakin panjang.


2. Dependency Injection Container dengan Uber-dig

Jika resolver mencapai puluhan, library seperti Uber Dig bisa sangat membantu. dig menyederhanakan pembuatan dependency graph & otomatis resolve dependency.

Instalasi

go get go.uber.org/dig

Implementasi dengan Uber-dig: Simulasi Resolver

Misal kita punya 3 resolver dari total 62 (untuk ringkas):

import (
    "database/sql"
    "go.uber.org/dig"
)

type UserService struct{ db *sql.DB }
type PostService struct{ db *sql.DB }
type CommentService struct{ db *sql.DB }

func NewUserService(db *sql.DB) *UserService      { return &UserService{db} }
func NewPostService(db *sql.DB) *PostService      { return &PostService{db} }
func NewCommentService(db *sql.DB) *CommentService{ return &CommentService{db} }

type UserResolver struct{ Service *UserService }
type PostResolver struct{ Service *PostService }
type CommentResolver struct{ Service *CommentService }

func NewUserResolver(svc *UserService) *UserResolver       { return &UserResolver{svc} }
func NewPostResolver(svc *PostService) *PostResolver       { return &PostResolver{svc} }
func NewCommentResolver(svc *CommentService) *CommentResolver { return &CommentResolver{svc} }

func main() {
    container := dig.New()
    container.Provide(sql.Open) // supply *sql.DB
    container.Provide(NewUserService)
    container.Provide(NewPostService)
    container.Provide(NewCommentService)
    container.Provide(NewUserResolver)
    container.Provide(NewPostResolver)
    container.Provide(NewCommentResolver)

    // resolve all resolvers
    container.Invoke(func(
        userResolver *UserResolver,
        postResolver *PostResolver,
        commentResolver *CommentResolver,
    ) {
        // bisa inject ke router, GraphQL server, dll
    })
}

Scalability: Cukup tambah .Provide() setiap resolver/service baru. Uber-dig menguraikan dependency graph dan inject secara otomatis, walaupun jumlahnya ratusan.


Diagram Alur Dependency Injection

Mari lihat gambaran wiring resolver dengan DI menggunakan mermaid:

graph TD
  subgraph Database Layer
    DB[(Database)]
  end

  subgraph Service Layer
    USV[UserService]
    PSV[PostService]
    CSV[CommentService]
  end

  subgraph Resolver Layer
    UR[UserResolver]
    PR[PostResolver]
    CR[CommentResolver]
  end

  DB --> USV
  DB --> PSV
  DB --> CSV

  USV --> UR
  PSV --> PR
  CSV --> CR

  %% Dst. hingga 62 resolver

Bayangkan diagram di atas membesar ke 62 node di layer Service & Resolver—tanpa DI container, wiring-nya sangat rawan error.


Studi Simulasi: Penambahan Resolver ke-63

Tanpa DI Container

  • Ubah constructor AppResolvers
  • Tambah dependency di main.go
  • Add/match di tiap service dan resolver
  • Tinggi risiko error

Dengan DI Container

  • Tambah .Provide(NewNewResolver) pada container
  • Implementasi tetap konsisten
  • No need to modify existing code

Tabel Simulasi Penambahan Dependency:

DependencyManual Wiring (Langkah)uber-dig (Langkah)
Kode di Resolver11
Kode di AppResolvers10
Kode di main.go2-31
Risiko Human ErrorSedangRendah

Testing Resolver dengan Dependency Injection

Keuntungan utama DI: testing lebih mudah! Kita bisa mock/services, inject dependency ke resolver, dan isolasi unit test.

func TestUserResolver_GetUser(t *testing.T) {
    mockService := &MockUserService{}
    resolver := NewUserResolver(mockService)
    // lanjutkan dengan pengujian
}

Kesimpulan

Mengelola 62+ resolver di project Go adalah tantangan serius jika dependensi tidak didesain dengan baik. Dependency Injection—baik manual maupun menggunakan container seperti Uber-dig—adalah solusi yang scalable, testable, dan maintainable. Meski Go tidak memiliki DI container built-in, pola constructor injection dan adopt library eksternal memberikan fleksibilitas tinggi untuk menangani codebase yang kompleks.

Jadikan DI sebagai standar baru ketika membangun proyek Go berskala besar—baik untuk resolver GraphQL, REST handler, atau service logic lainnya!


Referensi:


Semoga artikel ini membantu Anda membangun resolver yang scalable di Go!
Have fun coding 🚀

comments powered by Disqus

Topik Terhangat

programming
301
tutorial
168
tips-and-trick
44
jaringan
28
hardware
11
linux
4
kubernetes
1