Golang での DI をいい感じに解決するDI Containerを作ってみた
どうも、レベチの Gopher を目指している厚木です。
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()
サービスが大きくなるほどに 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 ツールはいわゆる 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 Container 内で、Usecase に Repository が渡されるので、上記のように呼び出すことができるようになります。
この方法であれば、DI する Function が増えたり、引数となる Interface が増えても呼び出し時や DI の負担はあまり増えません。
サンプルコード
以下サンプルコードです。
-
DI Container を利用した実装例
https://github.com/tkyatg/ditest-api -
今回作成した DI Container
https://github.com/tkyatg/example-golang-dicontainer
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 はどうしても再起的に処理を行う必要があるためです。(もしもっといい感じにできる方法をご存知の方はご教示ください!)
OSS DI ツール紹介
この記事の内容と同じようなことができる OSS の DI ツールです。
自作するほどでもないかな、という場合は以下のものがおすすめです。
https://github.com/uber-go/dig
https://github.com/google/wire
最後に
サービス規模の小さいものや、マイクロサービス化している場合は手動 DI すれば良いと思いますが
モノリシックな場合は DI ツールは使った方が良い場合が多いと思います。
あと業務委託のお仕事募集してます
連絡先: takuya.atsugi911@gmail.com
Discussion