Golang での DI をいい感じに解決するDI Containerを作ってみた
2021年4月から現場を移動したのですが、今の現場の DI が結構辛いことになっているので
いい感じに DI する方法を考えてみました。
あるあるDI(辛いやつ
よくある、肥大化していく DI の例です。
Usecase と Repository を作成し、DI してみます。
/usecase/usecase.go
package usecase
import (
repo "github.com/tkyatg/ditest-api/repository"
)
type (
usecase struct {
repo repo.Repository
}
Usecase interface {
Hello() string
}
)
func NewUsecase(repo repo.Repository) Usecase {
return &usecase{
repo,
}
}
func (t *usecase) Hello() string {
return t.repo.Hello()
}
/repository/repository.go
package repository
type (
Repository interface {
Hello() string
}
repository struct{}
)
func NewRepository() Repository {
return &repository{}
}
func (t *repository) Hello() string {
return "hello from repository"
}
上記のような実装をしている場合
Repository のHello()
を呼び出したい際は、以下のように呼び出す必要があります。
repo := repository.NewRepository()
uc := usecase.NewUsecase(repo)
uc.Hello() // usecaseの中でrepoの処理を呼んでいる
この呼び方をする場合、サービスが大きくなるほどに DI が辛くなっていきます。
例えば外部 SDK を使う場合や、その他諸々追加していくと以下のようになっていきます。
env := env.NewEnv()
awsClient := aws.NewClient(env)
gcpClient := gcp.NewClient(env)
sendgridClient := sendgrid.NewClient(env)
da := dataAccessor.NewDataAccessor(env)
repo := repository.NewRepository(da, awsClient, gcpClient, sendgridClient)
uc := usecase.NewUsecase(env, repo)
uc.Hello()
DIを改善してみる
今回作った DI ツールはいわゆる DI Container というやつです。
DI を自動的に解決するツールです。
Usecase と Repository を DI Container に登録する
container := dicontainer.NewContainer()
if err := container.Register(repository.NewRepository); err != nil {
log.Fatal(err)
}
if err := container.Register(usecase.NewUsecase); err != nil {
log.Fatal(err)
}
DI Container に登録した Usecase のHello()
を呼び出してみる
if err := container.Invoke(func(
uc usecase.Usecase,
) error {
uc.Hello()
return nil
}); err != nil {
log.Fatal(err)
}
この方法であれば、DI する Function が増えたり、引数となる Interface が増えても呼び出し時や DI の負担はあまり増えません。
DI Containerの実装について
以下、DI Container の実装についての説明です。
モチベーションがある方のみ読んでみてください。
ざっくりやるべきこと
DI Container を実装する時にやること
- DI Container を作成する。
- DI Container への登録時、DI 対象の Interface を作成する Function の引数、戻り値を覚えておく
- DI した Interface を呼び出す際、Interface の作成に必要な引数 Interface を作成し、対象の Interface を作成する
- 呼び出す Interface の作成に必要な引数の Interface を作成する際、引数として別の Interface を持っていた場合、再起的にこの処理を行う。
今回の場合、Usecase Interface、Repository Interface があって、両方を Container に登録している。
Usecase Interface には Repository Interface を作って、Inject する。
Repository Interface には作成に必要な引数がない。
もし Repository Interface に必要な引数があった場合は、それを作成し、Repository Interface に Inject する。その引数にも...ということを繰り返す。
結果的に全て DI が解決された Usecase Interface が取得できるというイメージ。
今回の実装
やるべきことと、実装した Function の関係性です。
- DI Container を作成する - NewContainer
- DI Container への登録時、DI 対象の Interface を作成する Function の引数、戻り値を覚えておく - Register
- DI した Interface を呼び出す際、Interface の作成に必要な引数 Interface を作成し、対象の Interface を作成する - Invoke
- 呼び出す Interface の作成に必要な引数の Interface を作成する際、引数として別の Interface を持っていた場合、再起的にこの処理を行う。 - resolve
実装時に気をつけること
もしご自身でDI Container 実装する際は Cash はなるべく行った方がいいです。
DI Container はどうしても再起的に処理を行う必要があるためです。(もしもっといい感じにできる方法をご存知の方はご教示ください!)
最後に
Github アカウントフォローしてもらえると嬉しいです。
Author And Source
この問題について(Golang での DI をいい感じに解決するDI Containerを作ってみた), 我々は、より多くの情報をここで見つけました https://zenn.dev/takuya911/articles/711d629934795f著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol