はじめに
開発しているGoのプロジェクトが大きくなってきたことでDependency Injection(以下DI)管理をすっきりさせたいと思い、他言語で使われるDIコンテナのようなものを探していたところ、現状(2019年12月時点)ではWireというライブラリ兼ツールがメジャーであるとのことで使ってみました。
スモールスタートで試せそうな手本となる記事がちょっと少なかった気がしたので具体例を踏まえた記事を今回作りました。
Wireのコンセプトの概要
公式のFAQを元にまとめてみました。
Wireはコードを自動生成するサポートによってコンパイル時でDIを提供するツールです。
コンパイル時という特徴により、初期化の考慮やguruなどの静的解析のツールとの相互参照が容易になります。
digやfacebookgo/injectといった他のGo製のDIツールはruntime時にリフレクションをすることでDIコンテナを提供しますが、これにはコードを実行してランタイムエラーが発生して初めて不整合が見つかるというデメリットがあります。
サンプルコードで試してみる
WireをDIのためにどう使うのかサンプルコードを使って進めてみます。
Wire 利用前
はじめにWire利用前の状態から。
├── handlers.go
├── main.go
├── tagservice # package
├── db.go
├── domain.go
└── repositories.go
上記はクリーンアーキテクチャを意識した
main.go -> handlers.go -> domain.go -> repositories.go -> db.go
という一直線の依存関係になっています。
domain.go
にはhandlerが利用するインターフェース、repositories.go
にはdomainが利用するインターフェースが定義されています。
type Config struct {
DSN string
}
type SqlDb struct {
*sql.DB
}
func NewDB(cfg *Config) SqlDb {
dsn := cfg.DSN
var err error
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
return SqlDb{db}
}
type ITagQueryRepository interface {
QueryTagsWhereTagType(tagTypeId uint8) ([]*TagQueryDto, error)
}
type realTagQueryRepository struct {
db SqlDb
}
func NewTagQueryRepository(db SqlDb) ITagQueryRepository {
return &realTagQueryRepository{
db: db,
}
}
type TagServer interface {
GetTagsByTagType(tagType TagType) ([]*CategoryTags, error)
}
type RealTagServer struct {
tagQueryRepository ITagQueryRepository
}
func NewTagServer(queryRepository ITagQueryRepository) TagServer {
return &RealTagServer{
tagQueryRepository: queryRepository,
}
}
type TagsHandler struct {
server tagservice.TagServer
}
func newTagsHandler(server tagservice.TagServer) *TagsHandler {
return &TagsHandler{
server: server,
}
}
func (h *TagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
import (
"github.com/momotaro98/tagservice" // fake
)
tagServiceConfig := &tagservice.Config{
DSN: dsn,
}
sqlDb := tagservice.NewDB(tagServiceConfig)
iTagQueryRepository := tagservice.NewTagQueryRepository(sqlDb)
tagServer := tagservice.NewTagServer(iTagQueryRepository)
tagsHandler := newTagsHandler(tagServer)
p.Add("GET", "/api/v1/tags", tagHandler) // routing
上記ではDIを実現することができおり、それぞれのレイヤのモックを作ることでテストも書くことができます。
しかし問題点として
- プロジェクトが大きくなると初期化処理が肥大化してしまう。
- 依存関係が複雑になるほど初期化処理が煩雑になってしまう。
- 利用者側は各レイヤ(handler, domain, repositories, db)のすべての初期化関数を知っていないといけない。→ 結合度が高くなる。
があげられます。
Wireを利用する
Wire利用後の構成
利用前と利用後の構成の差分を示します。
├── handlers.go
├── main.go
├── tagservice # package
│ ├── db.go
│ ├── domain.go
│ ├── provider.go # [New] wireの"Provider"用のファイル
│ └── repositories.go
├── wire.go # [New] wireの"Injector"用のファイル
└── wire_gen.go # [New] wireコマンドにて自動生成されたファイル
WireにはProviderとInjectorという2つのロールがあり、Providerはpackage側で定義しInjectorは利用者側で定義するのが一般的なようです。
Provider
Providerというのは初期化関数のことです。wire利用前からもっており、NewXXX
になっている関数がProviderです。
provider.go
という専用のファイルを作り、ここではProviderたちを一つにまとめて利用者側に提供するための値を定義します。
package tagservice
import (
"github.com/google/wire"
)
var SuperSet = wire.NewSet(
NewDB,
NewTagQueryRepository,
NewTagServer,
)
それぞれのレイヤーの初期化関数を引数にしてSuperSet
という変数を定義しました。これを下記のInjectorに渡します。
Injector
ProviderとまとめたSetを作ったらInjectorを作りましょう。
// +build wireinject
// ↑は必要なので一行目に手動で書く。その理由→The build tag makes sure the stub is not built in the final build.
package main
import (
"github.com/google/wire"
"github.com/momotaro98/tagservice"
)
func initializeTagsHandler(tagServiceConfig *tagservice.Config) *TagsHandler {
wire.Build(tagservice.SuperSet, newTagsHandler)
return nil
}
Injectorではmain.go
にとって欲しいのでハンドラーを返す関数を用意します。
wire.Build
関数にpackageで提供しているProviderのSetとそれを利用するハンドラーの初期化関数を渡します。
ここでの関数はwireコマンド用に利用されるもので、これをもとに実際に利用されるDI関数が生成されます。そのため返り値はnil
で問題なしです。
DI関数を生成
ProviderとInjectorを用意できたのでwireコマンドでDI関数を生成する準備が整いました。
$ wire
wire: github.com/momotaro98/xxx: wrote .../wire_gen.go
```go:生成されたwire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject
package main
import (
"github.com/momotaro98/tagservice"
)
// Injectors from wire.go:
func initializeTagsHandler(tagServiceConfig *tagservice.Config) *TagsHandler {
sqlDb := tagservice.NewDB(tagServiceConfig)
iTagQueryRepository := tagservice.NewTagQueryRepository(sqlDb)
tagServer := tagservice.NewServer(iTagQueryRepository)
tagsHandler := newTagsHandler(tagServer)
return tagsHandler
}
生成された関数はWire利用前にて`main.go`で書かれた処理とまったく一緒です。wireはベタ書きのDI初期化をあくまでも自動生成とチェックによって補助してくれるツールです。
```go:書き換わったmain.goの一部
tagServiceConfig := &tagservice.Config{
DSN: dsn,
}
p.Add("GET", "/api/v1/tags", initializeTagsHandler(tagServiceConfig)) // routing
main.go
内の処理は設定初期化だけになりすっきりしました。
また、wire_gen.go
にて//go:generate wire
も生成されたためこのあとはgo generate
を使って保守していくことができます。
はまりどころ : 返り値が同じ型の関数を同時にProviderにすることはできない
上記のサンプルコードではtagservice
という1つのpackageのみで扱っていましたが、実際は他にもuserservice
というpackageも同じ構成でありこれはtagserviceのメソッドを利用するため依存しています。db.go
にて
type SqlDb struct {
*sql.Db
}
のように専用の型をもっているのはそうしないとwireコマンドで生成する際、
wire: XXX/userservice/provider.go:8:16: SuperSet has multiple bindings for *database/sql.DB
current:
<- provider "ProvideDB" (XXX/userservice/db.go:11:6)
previous:
<- provider "ProvideDB" (XXX/tagservice/db.go:20:6)
wire: XXX: generate failed
このように同じ型(*database/sql.DB
)を返す関数が重複して存在するという理由でエラーになってしまいます。そのため、各packageごとにカスタムなSqlDb型を定義する必要がありました。
Provider Set内で同じ型を返す関数が複数存在するとInjectorがどちらを使えば良いか判別できないのが理由なようです。
なぜProviderの返り値の型の重複を許さないのかは公式のFAQでも言及されています。
こういった制約が存在するため、こちらの記事などでも述べられているように、Wireは一種のフレームワークと捉えることができ、スモールなGoのプロジェクトでは採用することは公式でもおすすめはされていないようです。
おわりに
一般的なDIコンテナが持つ複雑さがなく、あくまでも補助してくれているという感じでコードも見通しがつき使い勝手が良いと感じています。
確かに制約がありGo標準のピュアさが薄れライブラリ依存にはなりますが、そこはWebフレームワークとかORマッパーとかと同じ話でプロジェクト毎に考慮して利用するかしないかを選択する必要があるということですね。