# Runtime
Dependency
Injection in Go
Fukuoka.go #11 by Yuichi Watanabe @nulab
## Overview
Golang の DI ライブラリ vvatanabe/shot を書きました
-
所謂 DIコンテナ
- inspired by google/guice
- 実行時に動的にDI (Runtime DI) する
- 設定がコードベース
- オブジェクトの依存解決方法や生成タイミング等が指定できる
- Constructor Function、Struct Field、Object
- Singleton、Eager Singleton
object App {
def main(args: Array[String]) {
val injector = Guice.createInjector(new Module() {
override def configure(binder: Binder): Unit = {
binder.bind(classOf[UserRepository]).to(classOf[UserRepositoryOnDB])
binder.bind(classOf[GroupRepository]).to(classOf[GroupRepositoryOnDB])
}
})
val userRepository = injector.getInstance(classOf[UserRepository])
val groupRepository = injector.getInstance(classOf[GroupRepository])
}
}
func main() {
injector, err := CreateInjector(func(binder Binder) {
binder.Bind(new(UserRepository)).To(new(UserRepositoryOnDB))
binder.Bind(new(GroupRepository)).To(new(GroupRepositoryOnDB))
})
userRepository := injector.Get(new(UserRepository))
groupRepository := injector.Get(new(GroupRepository))
}
Scala:
Golang:
Like This
## Motivation
- DI自体は、依存しているオブジェクトを外から指定できるように書けばいいだけなので、汎用化されたライブラリを使わなくてもできる。
- しかし、その依存しているオブジェクトを外から指定するコードは結構地道な作業でちょっと気が乗りにくい。自動化したい。
- Golang のコードベースで DI の設定を書けるようにしとけば、型の定義と設定がずれにくくなる。IDEでリファクタリングしたときなど自動で置き換わってくれる。
- 普段の業務でScalaやJavaを書くなか、google/guice を使っていて慣れていたので、同じ様なインターフェースで Golang でも書いてみたい。
## Usage
package main
import (
"github.com/vvatanabe/shot/shot"
)
func main() {
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(UserRepository)).To(new(UserRepositoryOnDB))
binder.Bind(new(GroupRepository)).To(new(GroupRepositoryOnDB))
})
}
### Basis
Configure 関数にオブジェクトの生成ルールを記述すると、依存関係を解決してオブジェクトを生成する。
Constructor 関数で依存するオブジェクトをバインドする
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(GroupRepository)).ToConstructor(NewGroupRepositoryOnDB)
})
### Constructor Injection
### Field Injection
Struct の Field へ直接依存するオブジェクトをバインドする
type ProjectService struct {
UserRepository UserRepository `inject:""`
GroupRepository GroupRepository `inject:""`
}
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(ProjectService)).To(new(ProjectService))
})
生成済のオブジェクトを直接バインドする
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(Config)). ToInstance(&Config{})
})
### Instance Injection
シングルトンとしてオブジェクトを生成する
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(UserRepository)).
To(new(UserRepositoryOnDB)).In(shot.SingletonInstance)
binder.Bind(new(GroupRepository)).
To(new(GroupRepositoryOnDB)).AsEagerSingleton()
})
### Singleton
コンテナからオブジェクトを取得する
injector, err := shot.CreateInjector(func(binder shot.Binder) {
binder.Bind(new(UserRepository)).To(new(UserRepositoryOnDB))
binder.Bind(new(GroupRepository)).To(new(GroupRepositoryOnDB))
})
userRepository := injector.Get(new(UserRepository)).(UserRepository)
groupRepository := injector.Get(new(GroupRepository)).(GroupRepository)
### Get Object
userRepository, err := injector.SafeGet(new(UserRepository))
groupRepository, err := injector.SafeGet(new(GroupRepository))
コンテナからオブジェクトを安全に取得する
## Approach
- 本家と同じく依存するオブジェクトを解決するためにリフレクションを多様。
- ここではリフレクションでオブジェクトを注入する流れを、簡略化したコードベースで紹介。
### Constructor Injection
constructor 関数の reflect.Value と reflect.Type を取得する。
- reflect.Value: 値を扱う
- reflect.Type: 型を扱う
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
...
}
### Constructor Injection
constructor 関数自身の reflect.Type から、各引数の reflect.Type を取得する。
- NumIn(): 関数の引数の数を返す
- In(int): 関数の引数の reflect.Type を返す
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
var constructorArgs []reflect.Value
for i := 0; i < constructorType.NumIn(); i++ {
argType := constructorType.In(i)
...
}
### Constructor Injection
各引数の reflect.Type を元に、解決済のオブジェクトリストから引数に突っ込む値を取得する。
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
var constructorArgs []reflect.Value
for i := 0; i < constructorType.NumIn(); i++ {
argType := constructorType.In(i)
value := injector.Get(argType)
...
}
### Constructor Injection
引数に突っ込む値を、reflect.Value に変換してコンストラクタ関数に渡す引数の配列に追加する。
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
var constructorArgs []reflect.Value
for i := 0; i < constructorType.NumIn(); i++ {
argType := constructorType.In(i)
value := injector.Get(argType)
reflectValue := reflect.ValueOf(value)
constructorArgs = append(constructorArgs, reflectValue)
}
...
}
### Constructor Injection
コンストラクタ関数の reflect.Value が持つ Call 関数に、先程取得した引数の []reflect.Value を与えて実行する。
- func (v Value) Call(in []Value) []Value : 関数を実行する関数
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
var constructorArgs []reflect.Value
for i := 0; i < constructorType.NumIn(); i++ {
argType := constructorType.In(i)
value := injector.Get(argType)
reflectValue := reflect.ValueOf(value)
constructorArgs = append(constructorArgs, reflectValue)
}
results := constructor.Call(constructorArgs)
...
}
### Constructor Injection
コンストラクタ関数は1つだけ値を返すとゆう制約のもと、結果の配列の頭にDI済のオブジェクトが格納されている。
func Resolve(injector Injector, constructorFunc interface{}) interface{} {
constructor := reflect.ValueOf(constructorFunc)
constructorType := reflect.TypeOf(constructorFunc)
var constructorArgs []reflect.Value
for i := 0; i < constructorType.NumIn(); i++ {
argType := constructorType.In(i)
value := injector.Get(argType)
reflectValue := reflect.ValueOf(value)
constructorArgs = append(constructorArgs, reflectValue)
}
results := constructor.Call(constructorArgs)
return results[0].Interface()
}
### Constructor Injection
### Field Injection
### Field Injection
コンストラクタ関数は1つだけ値を返すとゆう制約のもと、結果の配列の頭にDI済のオブジェクトが格納されている。
func Resolve(injector Injector, structure interface{}) interface{} {
structureType := reflect.ValueOf(structure)
structureValue := reflect.TypeOf(structure)
...
}
### Field Injection
struct の reflect.Type 型 から NumField 関数 を 実行して フィールドの数を取得する。 Field 関数に indexを 渡して、フィールドの型情報 (reflect.StructField) と フィールドの値情報 (reflect.Value) を取得する。
func Resolve(injector Injector, structure interface{}) interface{} {
structureType := reflect.ValueOf(structure)
structureValue := reflect.TypeOf(structure)
for i := 0; i < structureType.NumField(); i++ {
structField := structureType.Field(i)
structFieldValue := structureValue.Field(i)
...
}
### Field Injection
それを元に解決済のオブジェクトリストからフィールドの値として実態を取得する。
func Resolve(injector Injector, structure interface{}) interface{} {
structureType := reflect.ValueOf(structure)
structureValue := reflect.TypeOf(structure)
for i := 0; i < structureType.NumField(); i++ {
structField := structureType.Field(i)
structFieldValue := structureValue.Field(i)
value := injector.Get(structField.Type)
...
}
### Field Injection
それを reflect.Value に変換して、Set 関数でフィールドにセットする。
func Resolve(injector Injector, structure interface{}) interface{} {
structureType := reflect.ValueOf(structure)
structureValue := reflect.TypeOf(structure)
for i := 0; i < structureType.NumField(); i++ {
structField := structureType.Field(i)
structFieldValue := structureValue.Field(i)
value := injector.Get(structField.Type)
reflectValue := reflect.ValueOf(value)
structFieldValue.Set(reflectValue)
...
}
### Field Injection
対象のオブジェクトは依存しているオブジェクトが解決された状態となる。
func Resolve(injector Injector, structure interface{}) interface{} {
structureType := reflect.ValueOf(structure)
structureValue := reflect.TypeOf(structure)
for i := 0; i < structureType.NumField(); i++ {
structField := structureType.Field(i)
structFieldValue := structureValue.Field(i)
value := injector.Get(structField.Type)
reflectValue := reflect.ValueOf(value)
structFieldValue.Set(reflectValue)
}
return structureValue.Interface()
}
## Impression & Issue
- Golang でも、 割と少量のコードで DI できるようになった。
- ユニットテスト時のモックの差し替えも vvatanabe/shot 経由でまとめてやれる。
// TODO サンプル書く
-
実行時に動的にDIしているので、必要なオブジェクトがバインドされていない時や、対象のinterfaceと突っ込むオブジェクトの型が合ってない等の設定ミスは、実行時にしかわからない。
- できればコンパイル時にエラーの有無を知りたい。
-
依存するオブジェクトを注入する処理を地道に書けば、もし漏れがある場合コンパイル時にエラーとなる。
-
地道に書くのは面倒くさい。
-
パターンは決まっているのでコードジェネレーションできそう。
- 次の感じで、所謂 コードジェネレーションを利用した、Compile time DI をやりたくなってきた。
package module
// @injector-gen-go
func GenerateInjector(binder Binder) {
binder.Bind(new(UserRepository)).To(new(UserRepositoryOnDB))
binder.Bind(new(GroupRepository)).To(new(GroupRepositoryOnDB))
binder.Bind(new(ProjectService)).AsSingleton()
})
こんな感じでコードベースの設定をパースして
// Code generated by injector-gen-go. DO NOT EDIT.
// source: module/module.go
package module
type Injector interface {
func GetUserRepository() UserRepository
func GroupRepository() GroupRepository
func ProjectService() *ProjectService
}
func NewInjector() Injector {
injector := &injector{}
return injector
}
type injector struct {
projectService *ProjectService
projectServiceOnce sync.Once
}
〜〜
こんなのを自動生成する
〜〜
func (i *injector) GetUserRepository() UserRepository {
return &UserRepositoryOnDB{}
}
func(i *injector) GroupRepository() GroupRepositoryOnDB {
return &GroupRepositoryOnDB{}
}
func(i *injector) GetProjectService() *ProjectService {
i.projectServiceOnce.Do(func() {
i.projectService = &ProjectService{
UserRepository: GetUserRepository(),
GroupRepository: GroupRepository(),
}
})
return i.projectService
}
次の Fukuoka.go までにやってみたい
os.Exit(0)
Runtime Dependency Injection in Go
By Yuichi Watanabe
Runtime Dependency Injection in Go
- 1,592