29
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go4Advent Calendar 2019

Day 11

GoのプロジェクトのDIをWireを使ってシンプルに

Last updated at Posted at 2019-12-10

はじめに

開発しているGoのプロジェクトが大きくなってきたことでDependency Injection(以下DI)管理をすっきりさせたいと思い、他言語で使われるDIコンテナのようなものを探していたところ、現状(2019年12月時点)ではWireというライブラリ兼ツールがメジャーであるとのことで使ってみました。
スモールスタートで試せそうな手本となる記事がちょっと少なかった気がしたので具体例を踏まえた記事を今回作りました。

Wireのコンセプトの概要

公式のFAQを元にまとめてみました。

Wireはコードを自動生成するサポートによってコンパイル時でDIを提供するツールです。
コンパイル時という特徴により、初期化の考慮やguruなどの静的解析のツールとの相互参照が容易になります。
digfacebookgo/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が利用するインターフェースが定義されています。

db.goの一部
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}
}
repositories.goの一部
type ITagQueryRepository interface {
	QueryTagsWhereTagType(tagTypeId uint8) ([]*TagQueryDto, error)
}

type realTagQueryRepository struct {
	db SqlDb
}

func NewTagQueryRepository(db SqlDb) ITagQueryRepository {
	return &realTagQueryRepository{
		db: db,
	}
}
domain.goの一部
type TagServer interface {
	GetTagsByTagType(tagType TagType) ([]*CategoryTags, error)
}

type RealTagServer struct {
	tagQueryRepository ITagQueryRepository
}

func NewTagServer(queryRepository ITagQueryRepository) TagServer {
	return &RealTagServer{
		tagQueryRepository: queryRepository,
	}
}
handlers.goの一部
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) {
main.goの一部
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たちを一つにまとめて利用者側に提供するための値を定義します。

provider.go
package tagservice

import (
	"github.com/google/wire"
)

var SuperSet = wire.NewSet(
	NewDB,
	NewTagQueryRepository,
	NewTagServer,
)

それぞれのレイヤーの初期化関数を引数にしてSuperSetという変数を定義しました。これを下記のInjectorに渡します。

Injector

ProviderとまとめたSetを作ったらInjectorを作りましょう。

wire.go
// +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マッパーとかと同じ話でプロジェクト毎に考慮して利用するかしないかを選択する必要があるということですね。

参考

29
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?