概要
こういった設計のようにレイヤ間の依存関係を抽象化していると、DIの初期化処理が段々冗長になっていきます。
google/wireはそういったDIの初期化を自動的にやってくれるコード生成ツールです。
今回はwireを使う上で知っていると便利なところをまとめます。
環境
wireの使い方知らない人
以下のチュートリアルが非常に分かりやすいので一度やってみてください。
実務で知っておくと良いポイント
Injectorの用意の手順
小さい依存関係であれば頭の中で把握できてサクッとかけますが、巨大なリポジトリになってくると何が何に依存しているか分からなくなってきます。
そういった場合まず最終的に用意するInjectorを生成するProviderだけwire.Build
するコードを用意し、エラーメッセージに書かれた通りにProviderを埋めていくとスムーズに進められます。
例
以下のようなモデルで、最終的にNewFooBarBaz()
を実行したい場合
type Foo int type Bar int type Baz int func ProvideFoo() Foo { return Foo(1) } func ProvideBar() Bar { return Bar(2) } func ProvideBaz() Baz { return Baz(3) } func NewFooBarBaz(foo Foo, bar Bar, baz Baz) int { return int(foo) + int(bar) + int(baz) }
まずはNewFooBarBaz
だけ書きます。
func initializeFooBarBaz() int { wire.Build( NewFooBarBaz, ) return 0 }
この状態でwire
すると、どの依存オブジェクトが足りないかを1つ1つ教えてくれます。
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Foo needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6) wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Bar needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6) wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Baz needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)
Foo
が足りないとあるので追加。
wire.Build( NewFooBarBaz, ProvideFoo, )
もう一度wire
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Bar needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6) wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/process.Baz needed by int in provider "NewFooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/process/model.go:19:6)
エラーが減りました。同じように足りないBar
やBaz
も追加していきます。
この例ではNewFooBarBaz
の引数だけ見ればすぐ分かっていいのですが、
- Barは別のオブジェクトに依存している
- Barが依存しているオブジェクトはさらに他のオブジェクトに依存している
という状態になってくると全部把握するのはなかなかツライです。
なのでこの手順で上から1つ1つ依存オブジェクトを埋めていくのをオススメします。
パッケージ毎にSetでまとめておく
実際の実装では様々なpackageに分かれています。
その場合どのproviderがどこにあるかを把握するのは大変です。
そういった場合は各package毎にproviderのグループを用意すると扱いやすくなります。
先ほどのモデルがmodel packageにあるとして、そのproviderを扱いたい場合
. ├── main.go ├── model │ └── model.go └── wire.go
model package内で以下のようにproviderグループを用意し、
var Set = wire.NewSet(
NewFooBarBaz,
ProvideFoo,
ProvideBar,
ProvideBaz,
)
injector側でそのグループを以下のように呼び出します。
func initializeFooBarBaz() int { wire.Build(model.Set) return 0 }
interfaceに依存している場合
CleanArchitectureでよく見る形ですが
type UserRepository interface { Save() error } type User struct { } func (u *User) Save() error { return nil } func NewUserRepository() *User { return &User{} } type UserService struct { repo UserRepository } func NewUserService(u UserRepository) *UserService { return &UserService{repo: u} }
上記コードのように、
- 依存しているのはinterface
- Provider(New関数)が生成するのは実装したstruct
である場合、その依存オブジェクトが実装されているstructを指定(bind)する必要があります。
func initializeUserService() *UserService { wire.Build( NewUserService, NewUserRepository, wire.Bind(new(UserRepository), new(*User)), // ← ) return &UserService{} }
このようにwire.Bind()
で実装したstructを指定します。
これはgoがduck typeを採用しており、実は同じメソッドを実装していたみたいなことが起きるため、間違ったオブジェクトを注入しないようにするためです。
structへの注入
先ほどはinterfaceのケースでしたが、structへ直接注入する方法もあります。
例えば以下のようなstructと各providerがあるとします。
type Foo int type Bar int type Baz int func ProvideFoo() Foo { return Foo(1) } func ProvideBar() Bar { return Bar(2) } func ProvideBaz() Baz { return Baz(3) } type FooBarBaz struct { MyFoo Foo MyBar Bar MyBaz Baz }
全フィールドに注入したい場合
wire.Struct()
と*
を使って注入できます。
func initializeFooBarBaz() *FooBarBaz { wire.Build( ProvideFoo, ProvideBar, ProvideBaz, wire.Struct(new(FooBarBaz), "*"), ) return &FooBarBaz{} }
wire
を実行すると以下のファイルが生成されます。
func initializeFooBarBaz() *FooBarBaz { foo := ProvideFoo() bar := ProvideBar() baz := ProvideBaz() fooBarBaz := &FooBarBaz{ MyFoo: foo, MyBar: bar, MyBaz: baz, } return fooBarBaz }
一部フィールドに注入したい場合
例えばMyFoo
とMyBar
フィールドだけ注入したい場合は
func initializeFooBarBaz() *FooBarBaz { wire.Build( ProvideFoo, ProvideBar, wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), ) return &FooBarBaz{} }
このようにFooとBar providerのみにして、wire.Struct
でフィールドを指定します。
wire
を実行すると以下のファイルが生成されます。
func initializeFooBarBaz() *FooBarBaz { foo := ProvideFoo() bar := ProvideBar() fooBarBaz := &FooBarBaz{ MyFoo: foo, MyBar: bar, } return fooBarBaz }
各エラー
よく見るエラーをそれぞれケースに応じて説明します。
no provider found for xxx
以下のように必要なProviderが入っていないと
wire.Build( ProvideFoo, wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), // ←MyBarのproviderがない )
no provider found
エラーが発生します。
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: no provider found for github.com/jun06t/go-sample/wire/struct/partial.Bar needed by *github.com/jun06t/go-sample/wire/struct/partial.FooBarBaz in struct provider "FooBarBaz" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:19:6)
multiple bindings for xxx
以下のように同じ依存オブジェクトを生成するProviderが含まれていると
wire.Build( ProvideFoo, ProvideBar, ProvideBar, // ← wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), )
multiple bindings
エラーが出ます。
wire: /xxx/wire.go:6:2: multiple bindings for github.com/jun06t/go-sample/wire/struct/partial.Bar current: <- provider "ProvideBar" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:11:6) previous: <- provider "ProvideBar" (/Users/a13156/.go/src/github.com/jun06t/go-sample/wire/struct/partial/model.go:11:6)
この例では同じ関数なのですぐに分かりますが、たくさんのパッケージでNewSet()で管理していたらいつの間にか重複するProviderが出てたりします。
またDBのmasterとslaveといった形で接続先を変えたい場合に
func NewDataRepository(master, slave *sql.DB) DataRepository { return &repoImpl{ master: master, slave: slave, } } func NewMaster() *sql.DB { /* .. */ } func NewSlave() *sql.DB { /* .. */ }
として
wire.Build( NewMaster, NewSlave, NewDataRepository, )
としてもmultiple bindings
エラーが出ます。wireは同じ型の依存オブジェクトを生成するProviderを許容しないためです。
What if my dependency graph has two dependencies of the same type?
公式では回避策として型を別途定義するよう薦めています。
struct has multiple fields of type
structに同じ型のフィールドを入れていると
type FooBarBaz struct { MyFoo Foo MyFoo2 Foo } func initializeFooBarBaz() *FooBarBaz { wire.Build( ProvideFoo, wire.Struct(new(FooBarBaz), "*"), ) return &FooBarBaz{} }
struct has multiple fields of type
エラーが出ます。
wire: /xxx/model.go:20:2: provider struct has multiple fields of type github.com/jun06t/go-sample/wire/struct/partial.Foo
先ほどと同じように、wireがどのフィールドにどのオブジェクトを注入すればいいか分からなくなるためです。
複数のProviderで依存オブジェクトが重複するのは問題ない
注意なのが1つのProviderの中で重複する引数や重複するフィールドを持っているとNGなだけであって、複数のProviderで依存オブジェクトが重複するのは問題ないです。
type Foo int type Bar int type Baz int func ProvideFoo() Foo { return Foo(1) } func ProvideBar(f Foo) Bar { return Bar(f) } func ProvideBaz(f Foo) Baz { return Baz(f) } func NewBarBaz(bar Bar, baz Baz) int { return int(bar) + int(baz) }
このようにProvideBar
とProvideBaz
は同じ依存オブジェクトを持っていますが
func initializeBarBaz() int { wire.Build( ProvideFoo, ProvideBar, ProvideBaz, NewBarBaz, ) return 0 }
これを実行しても問題なくビルドできます。
unused provider "xxx"
以下のように依存していない余分なproviderが入っていると
wire.Build( ProvideFoo, ProvideBar, ProvideBaz, // ← wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), )
unused provider
エラーが発生します。
wire: /xxx/wire.go:5:1: inject initializeFooBarBaz: unused provider "foo.ProvideBaz" wire: github.com/jun06t/go-sample/wire/struct/partial: generate failed wire: at least one generate failure
Bazは不要なのにProviderBazを含めてしまったせいですね。
NewSetでまとめる回避策
回避したい場合、不要なproviderを消すのは当然浮かびますが、NewSetを使って1つにまとめるという手法もあります。
Feature request: ignore unused provider set · Issue #227 · google/wire · GitHub
func initializeFooBarBaz() *FooBarBaz { wire.Build( Set, ) return &FooBarBaz{} } var Set = wire.NewSet( ProvideFoo, ProvideBar, ProvideBaz, wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), )
これで実行するとコケず、wire_gen.goは必要なものだけに絞られて生成されます。
func initializeFooBarBaz() *FooBarBaz { foo := ProvideFoo() bar := ProvideBar() fooBarBaz := &FooBarBaz{ MyFoo: foo, MyBar: bar, } return fooBarBaz } // wire.go: var Set = wire.NewSet( ProvideFoo, ProvideBar, ProvideBaz, wire.Struct(new(FooBarBaz), "MyFoo", "MyBar"), )
サンプルコード
今回のサンプルコードは以下にあります。