Kali ini kita akan memcoba membuat dependency Injection menggunakan Library Wire dari github.com/google/wire. Kegunaan dari library ini yaitu kita bisa membuat dependency yang begitu banyak bisa kita generate langsung dengan ringkas dan mudah.
Dalam hal ini Dependency Injection merupakan sebuah teknik yang mana sebuah objek menerima objek lain yang dibutuhkan ketika pembuatan objek itu sendiri. Biasanya objek yang menerima itu disebut client dan proses mengirim dependencies ke objek tersebut disebut inject. Sebenarnya dependency injection ini sering kita gunakan seperti Handler yang membutuhkan dependencies dari Usecase, atau membuat objek Usecase yang membutuhkan dependencies dari objek Repository.
Dalam pemrograman berorientasi objek sering kita sebut dengan Constructor yang mana ini adalah fungsi yang membuat sebuah objek. Biasanya dalam Golang kita membuat fungsi mirip dengan membuat objek baru. Misalnya pada usecase seperti ini.
func New(repo repository.ArticleRepository) *Usecase {
return &Usecase{articleRepository: repo}
}
Buat fungsi tersebut akan mudah kita buat karena dependency yang kita butuhkan hanya sedikit. Namun saat kode projek kita semakin besar maka akan semakin banyak dependency yang perlu diinjek sehingga kita perlu cara mempermudah injection ini dengan memanfaatkan library.
Pada projek yang sudah kita buat ini kita sudah melakukan manual dependency injection seperti kode dibawah ini.
func main() {
fileEnv := ".env"
if os.Getenv("environment") == "development" {
fileEnv = "../.env"
}
err := godotenv.Load(fileEnv)
if err != nil {
log.Fatalf("error loading .env file")
}
// inisialisasi database
db := database.New()
// inisialisasi repository
repository := mysqlRepository.New(db)
// inisialisasi usecase
articleUsecase := articleUsecase.New(repository)
// inisialisasi handler
articleHandler := httpHandler.New(articleUsecase)
// // inisialisasi new router
router := NewRouter(articleHandler)
server := http.Server{
Addr: "localhost:3000",
Handler: router,
}
err = server.ListenAndServe()
if err != nil {
panic(err)
}
}
Library Dependency Injection
Banyak sekali library yang telah disediakan di Internet dan beberapa repository github jika kalian mencoba mencarinya tetapi ada beberapa yang paling banyak digunakan diantaranya
- https://github.com/google/wire
- https://github.com/uber-go/fx
- https://github.com/golobby/container
- dan masih banyak lainnya
Pada kali ini kita akan mencoba library dari Google yaitu Wire sebagai library dependency injection yang akan kita gunakan. Salah satu yang kita pilih ini adalah library paling populer dari Golang. Dan selain itu Google Wire ini merupakan library yang berbasis compile yang artinya kode-nya akan kita generate bukan menggunakan reflection, sehingga Google Wire ini menjadi lebih cepat, karena hasil dari kompilasi adalah kode yang sudah digenerate tanpa menggunakan reflection lagi.
Menambahkan Dependency Google Wire
Masuk ke dalam projek yang lalu buka terminal dan ketik perintah dibawah ini.
go get github.com/google/wire
Selanjutnya untuk melakukan generate file dependency injection kita juga perlu meng-install library terminal-nya dengan perintah ini.
go install github.com/google/wire/cmd/wire@latest
Jika sudah terinstall maka akan terlihat pada terminal seperti dibawah ini.
➜ wire help
Usage: wire <flags> <subcommand> <subcommand args>
Subcommands:
check print any Wire errors found
commands list all command names
diff output a diff between existing wire_gen.go files and what gen would generate
flags describe all known top-level flags
gen generate the wire_gen.go file for each package
help describe subcommands and their syntax
show describe all top-level provider sets
➜ learn-golang-restful git:(main) ✗
Pengenalan Provider
Dependency Injection itu kita memerlukan bentuk constuctor yang dalam Google Wire ini disebut dengan Provider.
Misalkan contoh dari projek kita yaitu pada usecase.
type ArticleRepository struct {
}
type ArticleUsecase struct {
*ArticleRepository
}
Maka biasanya kita akan membuat constructor atau provider-nya seperti ini.
func New() *ArticleStore {
return &ArticleStore{}
}
func New(repo repository.ArticleRepository) *Usecase {
return &Usecase{
articleRepository: repo
}
}
Pengenalan Injector
Selah kita membuat provider, selanjutnya kita perlu membuat injector yang mana adalah sebuah function constructor yang memiliki isi berupa konfigurasi yang kita informasikan ke Google Wire (generator). Injector ini tidak akan kita pakai dalam program tetapi injector ini adalah fungsi yang akan digunakan oleh Google Wire untuk melakukan auto generate kode dependency injection.
Ketika kita membuat file injector kita perlu menambahkan pada baris pertama pada file tersebut seperti ini.
//go:build wireinject
// +build wireinject
Setelah mengenal injector dan provider langkah selanjutnya yaitu kita akan menggunakan terminal command line Google Wire untuk melakukan auto generate kode dependency injection. Perintah itu akan berjalan pada folder dan membaca package yang ada pada folder tersebut lalu akan terbuat file wire_gen.go
yang isinya adalah kode otomatis dependency injection.
Provider Set
Google Wire memiliki fitur ini untuk melakukan grouping Provider. Provider set ini sangat berguna ketika kode program kita sudah banyak sehingga lebih mudah untuk dibaca ketika melakukan grouping data Provider-nya. Contoh penerapan Provider misalnya seperti dibawah ini.
var articleSet = wire.NewSet(mysqlRepository.New, postgresRepository.New)
var categorySet = wire.NewSet(mysqlRepository.New, postgresRepository.New)
func InizializedArticleUsecase() usecase.ArticleUsecase {
wire.Build(articleSet, categorySet, usecase.New)
}
Binding Interface
Kita biasanya menggunakan interface sebagai kontrak struct. Secara default, Google Wire menggunakan tipe data asli untuk melakukan dependency injection, sehingga jika terdapat parameter berupa interface maka tidak ada Provider yang mengembalikan interface tersebut maka dianggap error. Pada kasus seperti ini kita bisa memberi tahu Google Wire jika ingin melakukan binding interface dengan meberi sebuah interface yang menggunakan provider dengan tipe sesuai.
Pada projek ini kita bisa melihat pada repository ataupun usecase yang memiliki struct interface maka saat pembuatan injector-nya pasti menggunakan binding. Contoh-nya seperti dibawah ini.
wire.Build(
mysqlRepository.New,
wire.Bind(new(repository.ArticleRepository), new(*mysqlRepository.ArticleStore)),
)
Struct Provider
Kita bisa membuat Struct Provider yang mana struct yang bida kita jadikan sebagai Provider secara otomatis kita sebut menjadi Provider. Maka jika kita ingin melakukan dependency injection terhadap field yang terdapat pada struct tersebut kita cukup menyebutkan nama filed yang akan diinject atau lakukan injection ke dalam semua filed dengan menggunakan karakter * (bintang). Contoh seperti dibawah ini.
wire.Build(
Usecase,
wire.Struct(new(repository.ArticleStore),"config")
)
Implementasi Auto Generate Dependency Injection Pada Projek
Sebelumnya kode pada file main.go
itu kita membuat manual dependency injection, maka sekarang kita akan mencoba mengubah menggunakan generator Google Wire. Pertama kita buat folder pada pkg/wire/
dan buat file injector.go
lalu isi pada file tersebut seperti dibawah ini.
//go:build wireinject
// +build wireinject
package wire
import (
"net/http"
"github.com/google/wire"
httpHandler "github.com/santekno/learn-golang-restful/internal/delivery/http"
"github.com/santekno/learn-golang-restful/internal/repository"
mysqlRepository "github.com/santekno/learn-golang-restful/internal/repository/mysql"
"github.com/santekno/learn-golang-restful/internal/usecase"
articleUsecase "github.com/santekno/learn-golang-restful/internal/usecase/article"
"github.com/santekno/learn-golang-restful/pkg/database"
"github.com/santekno/learn-golang-restful/pkg/router"
)
var articleSet = wire.NewSet(
mysqlRepository.New,
wire.Bind(new(repository.ArticleRepository), new(*mysqlRepository.ArticleStore)),
articleUsecase.New,
wire.Bind(new(usecase.ArticleUsecase), new(*articleUsecase.Usecase)),
httpHandler.New,
)
func InitializedServer() *http.Server {
wire.Build(
database.New,
articleSet,
router.NewRouter,
router.NewServer,
)
return nil
}
Pertama kita buat articleSet untuk melakukan grouping pada article repository, usecase dan handler dengan membuat seperti ini.
var articleSet = wire.NewSet(
mysqlRepository.New,
wire.Bind(new(repository.ArticleRepository), new(*mysqlRepository.ArticleStore)),
articleUsecase.New,
wire.Bind(new(usecase.ArticleUsecase), new(*articleUsecase.Usecase)),
httpHandler.New,
)
Disini bisa kita lihat mysqlRepository.New
memiliki return Interface maka kita perlu membuat binding dari repository Interface article ke dalam implementasinya maka akan terlihat seperti dibawah ini.
mysqlRepository.New,
wire.Bind(new(repository.ArticleRepository), new(*mysqlRepository.ArticleStore)),
Begitu pun sama halnya ketika usecase perlu memiliki interface terhadap handler, maka akan membuat binding seperti ini.
articleUsecase.New,
wire.Bind(new(usecase.ArticleUsecase), new(*articleUsecase.Usecase)),
Dan terakhir kita membuat fungsi inisialisasi yang memberikan return *http.Server
.
func InitializedServer() *http.Server {
wire.Build(
database.New,
articleSet,
router.NewRouter,
router.NewServer,
)
return nil
}
Setelah selesai menyelesaikan injector kita akan coba melakukan auto generate dengan perintah dibawah ini.
wire
Dan akan terbuat file dengan nama wire_gen.go
dengan isi file seperti ini.
// Code generated by Wire. DO NOT EDIT.
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package wire
import (
"github.com/google/wire"
http2 "github.com/santekno/learn-golang-restful/internal/delivery/http"
"github.com/santekno/learn-golang-restful/internal/repository"
"github.com/santekno/learn-golang-restful/internal/repository/mysql"
"github.com/santekno/learn-golang-restful/internal/usecase"
"github.com/santekno/learn-golang-restful/internal/usecase/article"
"github.com/santekno/learn-golang-restful/pkg/database"
"github.com/santekno/learn-golang-restful/pkg/router"
"net/http"
)
// Injectors from injector.go:
func InitializedServer() *http.Server {
db := database.New()
articleStore := mysql.New(db)
usecase := article.New(articleStore)
delivery := http2.New(usecase)
httprouterRouter := router.NewRouter(delivery)
server := router.NewServer(httprouterRouter)
return server
}
// injector.go:
var articleSet = wire.NewSet(mysql.New, wire.Bind(new(repository.ArticleRepository), new(*mysql.ArticleStore)), article.New, wire.Bind(new(usecase.ArticleUsecase), new(*article.Usecase)), http2.New)
Nah tinggal kita ubah sedikit file pada main.go
menjadi seperti ini.
package main
import (
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/joho/godotenv"
"github.com/santekno/learn-golang-restful/pkg/wire"
)
func main() {
fileEnv := ".env"
if os.Getenv("environment") == "development" {
fileEnv = "../.env"
}
err := godotenv.Load(fileEnv)
if err != nil {
log.Fatalf("error loading .env file")
}
server := wire.InitializedServer()
err = server.ListenAndServe()
if err != nil {
panic(err)
}
}
Lebih simpel bukan? memang pada tahapan awal kita agak ribet dan banyak sekali yang akan kita definisikan tetapi ke depannya jika kita ingin menambahkan provider yang baru tidak dimulai lagi dari awal tetapi hanya tinggal menambahkan newSet ataupun binding-nya saja.