💡

Golang での DI をいい感じに解決するDI Containerを作ってみた

2022/04/02に公開

どうも、レベチの 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の実装について

以下、DI Container の実装についての説明です。
モチベーションがある方のみ読んでみてください。

どのように実現するか

DI Container を実装する時にやること

  1. DI Container を作成する。
  2. DI Container への登録時、DI 対象の Interface を作成する Function の引数、戻り値を覚えておく
  3. DI した Interface を呼び出す際、Interface の作成に必要な引数 Interface を作成し、対象の Interface を作成する
    1. 呼び出す Interface の作成に必要な引数の Interface を作成する際、引数として別の Interface を持っていた場合、再起的にこの処理を行う。

今回の場合、Usecase Interface、Repository Interface があって、両方を Container に登録している。
Usecase Interface には Repository Interface を作って、Inject する。
Repository Interface には作成に必要な引数がない。
もし Repository Interface に必要な引数があった場合は、それを作成し、Repository Interface に Inject する。その引数にも...ということを繰り返す。

結果的に全て DI が解決された Usecase Interface が取得できるというイメージ。

今回の実装

やるべきことと、実装した Function の関係性です。

  1. DI Container を作成する - NewContainer
  2. DI Container への登録時、DI 対象の Interface を作成する Function の引数、戻り値を覚えておく - Register
  3. DI した Interface を呼び出す際、Interface の作成に必要な引数 Interface を作成し、対象の Interface を作成する - Invoke
    1. 呼び出す 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

GitHubで編集を提案

Discussion