GoのDIライブラリWireを試す
はじめに
GoのWebアプリは公私でなんとが作成したことがあったのですが、DIのライブラリをあまり使ったことが無かったなとふと思い。探してみたらGoogle製のアプリ、Wireがなんとなく目立ってたような気がしたので、Hello, worldとしてチュートリアルをやりつつ理解を深めようかと思います。
やってみる
環境
$ go version go version go1.16 linux/amd64 $ uname -srvmpio Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal
Wireの概要
DIをする際の課題の1つが初期化処理が煩雑になりがちというとこがあります。
Wireは構造体、初期化関数、それらの依存関係を定義する関数を作成してやり、wire
コマンドを実行すると、一連の初期化処理を自動生成してくれます。
基本的な流れ
Wireを使う際の基本的な流れは以下のようになります。
- DI対象の構造体を作成
- 構造体の初期化関数(プロバイダー)を作る
- 初期化関数の依存関係を定義するインジェクターを作成
wire
コマンドを使って初期化関数を作成
実際にDIしてみる
DI対象の構造体を作成
まずはDI対象の構造体を3つほどと1つのインターフェース作っておきます。
今回はケーキショップをモデルとした構造体をいくつか作っておきます。
(ちょっとモデリングがいびつかもしれませんが、適当に作っただけなのでご容赦ください)
栗を表す構造体
type Chestnut struct { }
ケーキインターフェースとモンブランを表す構造体
type Cake interface { Eaten() } type MontBlanc struct { Chestnut Chestnut } func (m MontBlanc) Eaten() { fmt.Printf("I'm MontBlanc, How is it?") }
ケーキショップを表す構造体
type CakeShop struct { Cake Cake } func (c CakeShop) Sell() { c.Cake.Eaten() }
構造としては、モンブランは栗(Chesnut)に依存し、ケーキショップはCakeインターフェースを通してモンブランなどのEaten()
関数を呼び出します。
構造体の初期化関数(プロバイダー)を作る
以下のように、作った構造体の初期化関数であるプロバイダーを作成しておきます。
func ChestnutProvider() Chestnut { return Chestnut{} } func MontBlancProvider(c Chestnut) MontBlanc { return MontBlanc{Chestnut: c} } func CakeShopWithMontBlancProvider(c MontBlanc) CakeShop { return CakeShop{Cake: c} }
それぞれ、書いてあるとおりではChestnutProvider()
はChestnut
を作成してMontBlancProvider(c Chestnut)
は受け取った栗を使ってMontBlanc
を作成します。そして、最後のCakeShopWithMontBlancProvider(c MontBlanc)
はCakeインターフェースを実装しているMontBlancを受け取ってCakeShopを作成するプロバイダーになってます。
初期化関数の依存関係を定義するインジェクターを作成
実装としては最後であるインジェクターの作成を行います。
ここでは、プロバイダーの依存関係を定義することができます。
// +build wireinject package config import ( "github.com/google/wire" "github.com/samuraiball/go-sandbox/message" "github.com/samuraiball/go-sandbox/sweets" ) func InitializeCakeShop() sweets.CakeShop { wire.Build( sweets.CakeShopWithMontBlancProvider, sweets.MontBlancProvider, sweets.ChestnutProvider, ) return sweets.CakeShop{} }
Wireでは、wire.Build()
の中でそれぞれの初期化に使うプロバイダーを記述していきます。そして、それぞれのプロバイダーで初期化される構造体が、別のプロバイダーの引数として利用されることになります(これは後ほどもう少し細かく説明します)。
ここで、コンパイルエラーを避けるためにインジェクターの戻り値では空のCakeShop{}
を返しています。このCakeShop{}
は実際は使われることはなく単純に無視されます。なので、ここで値を初期化して入れていたとしても意味がありません(// +build wireinject
を先頭に記述することで、ビルドの対象外にしています)。
wire
コマンドを使って初期化関数を作成
wire
コマンドをインジェクターが定義されているディレクトリーで実行します。するとwire_gen.go
というコードが自動生成されます。
// Code generated by Wire. DO NOT EDIT. //go:generate go run github.com/google/wire/cmd/wire //+build !wireinject package config import ( "github.com/samuraiball/go-sandbox/sweets" ) // Injectors from dependency.go: func InitializeCakeShop() sweets.CakeShop { chestnut := sweets.ChestnutProvider() montBlanc := sweets.MontBlancProvider(chestnut) cakeShop := sweets.CakeShopWithMontBlancProvider(montBlanc) return cakeShop }
ここではインジェクターと同じ名前の関数で、初期化処理のコードが生成されているのが見て取れます。
Wireの基本的な使い方はここまでです。
最後に処理をmain関数から呼び出してみます。
package main import "github.com/samuraiball/go-sandbox/config" func main() { shop := config.InitializeCakeShop() shop.Sell() }
実行結果
$ go build $ ./go-sandbox I'm MontBlanc, How is it?
最後に
今回の例はコンポーネント間の依存関係も複雑では無かったので、そこまで恩恵が得られませんでしたが(と言うより、手数がむしろ増えた気がします)、より複雑なアプリケーションを作成したり、テストをやりだしたりすると、プロバイダーとインジェクターの組み合わせで結構自由にDIの設定を記述できそうな感じがしました。