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 を実装する時にやること

  1. DI Container を作成する。
  2. DI Container への登録時、DI 対象の Interface を作成する Function の引数、戻り値を覚えておく
  3. 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 の関係性です。

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

実装時に気をつけること

もしご自身でDI Container 実装する際は Cash はなるべく行った方がいいです。
DI Container はどうしても再起的に処理を行う必要があるためです。(もしもっといい感じにできる方法をご存知の方はご教示ください!)

最後に

Github アカウントフォローしてもらえると嬉しいです。

https://github.com/tkyatg